In Part 2, we built our first real-world Server class and explored one of the most important concepts in object-oriented design:

Putting data and behaviour together.

Now we’re going to examine a concept that tends to attract both excitement and abuse:

Inheritance.

Used appropriately, inheritance can eliminate duplicate code and create elegant solutions. Used poorly, it can turn a relatively simple automation project into a nightmare of dependencies, side effects, and confusion. Like most advanced programming concepts, inheritance isn’t inherently good or bad. It’s simply another tool. The challenge is knowing when to use it.

Understanding the Problem

Let’s return to our infrastructure example.

Suppose we’re responsible for several types of systems:

Physical Servers
Virtual Machines
Cloud Instances

At first glance, these seem different. But are they really? Each one has a name, an operating system, an IP address, connectivity checks and health monitoring. This immediately suggests shared behaviour.

Without inheritance, we often end up with classes like this:

class PhysicalServer {}
class VirtualMachine {}
class CloudInstance {}

And duplicated properties:

[string]$Name
[string]$IPAddress
[bool]$Online

Duplicated methods:

TestConnection()
Refresh()
GetHealthStatus()

As we all know duplicated code rarely ages well.

The Base Class Concept

Inheritance allows us to capture common behaviour once. Instead of:

Physical Server
Virtual Machine
Cloud Instance

we identify the shared concept Computer. Every infrastructure object is ultimately a computer. Let’s create a base class.

class Computer {

    [string]$Name
    [string]$IPAddress
    [bool]$Online

    Computer([string]$Name) {
        $this.Name = $Name
    }

    [bool] TestConnection() {
        $this.Online = Test-Connection -ComputerName $this.Name -Count 1 -Quiet

        return $this.Online
    }
}

Now we’ve modelled behaviour common to every computer-type object.

Creating Child Classes

A child class inherits everything from its parent.

class VirtualMachine : Computer {

}

Creating an object:

$vm = [VirtualMachine]::new("WEB01")

Even though the class appears empty, it already has available:

Name
IPAddress
Online
TestConnection()

Example:

$vm.TestConnection()

The functionality comes from the parent. This is inheritance in its simplest form.

Adding Specialised Properties

The real value appears when child classes add their own functionality.

A virtual machine might have:

class VirtualMachine : Computer {

    [string]$Hypervisor
}

Usage:

$vm = [VirtualMachine]::new("WEB01")

$vm.Hypervisor = "Hyper-V"

A physical server might have:

class PhysicalServer : Computer {

    [string]$Rack
}

Usage:

$server = [PhysicalServer]@{ Name = 'Server01'; Rack = '42' }

Shared functionality remains centralised while specialised functionality stays where it belongs.

Constructor Inheritance

One aspect that often surprises PowerShell users is how constructors behave with inheritance.

Given:

class Computer {

    [string]$Name

    Computer([string]$Name) {
        $this.Name = $Name
    }
}

The child class can invoke the parent constructor:

class VirtualMachine : Computer {

    [string]$Hypervisor

    VirtualMachine(
        [string]$Name,
        [string]$Hypervisor
    ) : base($Name) {

        $this.Hypervisor = $Hypervisor
    }
}

Usage:

$vm = [VirtualMachine]::new("WEB01","Hyper-V")

Result:

Name       : WEB01
Hypervisor : Hyper-V

The parent handles common setup. The child handles specialised setup.

Inheriting Methods

Methods are inherited just like properties.

Base class:

class Computer {

    [string]$Name

    [void] Restart() {

        Restart-Computer -ComputerName $this.Name -Force
    }
}

Child class:

class CloudInstance : Computer {

}

Usage:

$instance = [CloudInstance]::new("CloudInstance01")
$instance.Restart()

No need to duplicate restart logic across every infrastructure class.

The First Warning Sign

At this point inheritance looks amazing. In fact, it often looks too amazing. This is where teams begin creating structures like:

Computer
│
├── Server
│   ├── PhysicalServer
│   ├── VirtualMachine
│   │   ├── HyperVVM
│   │   ├── VMwareVM
│   │   └── AzureVM
│   └── LinuxServer

What started as a simple model has become a hierarchy worthy of a family genealogy project. This is usually where trouble begins.

A Practical Example of Overengineering

Imagine somebody wants to add backup functionality. Soon we see:

class BackupCapableServer : Server {}

Then:

class BackupCapableLinuxServer : LinuxServer {}

Then:

class BackupCapableAzureLinuxServer : AzureLinuxServer {}

The class hierarchy starts exploding. Instead of simplifying the codebase, inheritance has increased complexity. This is one reason experienced software developers often repeat:

Prefer composition over inheritance.

Composition: The Alternative

Rather than inheriting behaviour, an object can contain another object. For example:

class BackupConfiguration {

    [int]$RetentionDays
    [string]$BackupLocation
}

Then:

class Server {

    [string]$Name

    [BackupConfiguration]$Backup
}

Usage:

$server.Backup.RetentionDays = 30

Instead of creating endless specialised child classes, we combine simpler building blocks. This often produces cleaner designs.

A Real-World Rule of Thumb

Use inheritance when:

A Virtual Machine IS A Computer
A Physical Server IS A Computer
An Azure VM IS A Computer

Use composition when:

A Server HAS A Backup Configuration
A Server HAS Monitoring Rules
A Server HAS Certificates

The wording matters.

If you can naturally say:

“X is a Y”

inheritance may fit.

If you naturally say:

“X has a Y”

composition is often better.

Making Inheritance Useful

Let me give an example of a practical structure.

Computer
│
├── PhysicalServer
├── VirtualMachine
└── CloudInstance

Shared functionality:

TestConnection()
Refresh()
GetHealthStatus()

Physical server functionality:

Rack
WarrantyExpiration

Virtual machine functionality:

Hypervisor
SnapshotCount

Cloud functionality:

SubscriptionId
Region
ResourceGroup

This remains easy to understand because the hierarchy is shallow and logical.

Avoid the “Future Requirement” Trap

One of the most common mistakes is designing inheritance structures for requirements that do not exist.

Developers often build:

BaseResource
ManagedResource
MonitoringCapableResource
CloudCapableResource
BackupCapableResource

long before any real use case appears. The result:

50% architecture
50% implementation

Many successful automation solutions instead start with:

class Server {}

and evolve only when necessary.

The Most Important Question

Whenever you’re creating a child class, ask yourself:

“Am I removing duplication, or am I creating abstraction?”

Removing duplication is usually valuable. Creating abstraction purely because it feels architectural usually isn’t. The best class hierarchies often emerge naturally from real requirements.

Conclusion

Inheritance is one of the most powerful features available in PowerShell classes. It allows us to create richer object models while avoiding duplication. But the goal isn’t to create impressive diagrams. The goal is to make automation easier to understand six months from now when the original author is on holiday and somebody else needs to fix production at 2 AM. Good inheritance reduces cognitive load. Bad inheritance becomes cognitive load. Knowing the difference is one of the defining characteristics of an experienced automation engineer.

Next in This Series

Part 4: Building Reusable Automation Frameworks

We’ll move beyond individual classes and explore how multiple classes can work together to create maintainable automation solutions.

Topics include:

  • Separating responsibilities
  • Service classes
  • Configuration objects
  • Dependency management
  • Framework structure
  • Organising classes into PowerShell modules

By the end, we’ll have the foundation of a lightweight automation framework built entirely with PowerShell classes.

Leave a Reply

Your email address will not be published. Required fields are marked *