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