In Part 1, we discussed why PowerShell classes exist and when they might make sense. Now it is time to answer the question every PowerShell professional eventually asks:
“This is all nice in theory, but what does a real-world class actually look like?”
Instead of diving into abstract examples, we’re going to take a script that many administrators and automation engineers have written at some point.
A server health checker.
We’ll start with a traditional PowerShell implementation and gradually evolve it into a class-based design. Along the way we’ll discover where classes genuinely help and where they might simply add complexity.
The Traditional Script
Imagine we have a script that performs basic health checks against a list of servers. It checks:
- Connectivity
- Last boot time
- Disk usage
A simplified version might look like this:
$servers = @(
"SRV01",
"SRV02",
"SRV03"
)
$results = foreach ($server in $servers) {
$online = Test-Connection -ComputerName $server -Count 1 -Quiet
if ($online) {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $server
[PSCustomObject]@{
Name = $server
Online = $true
LastBootTime = $os.LastBootUpTime
}
} else {
[PSCustomObject]@{
Name = $server
Online = $false
LastBootTime = $null
}
}
}
There is nothing wrong with this script. In fact, it follows common PowerShell practices and would be completely acceptable in many environments. But let’s see what happens when requirements start growing.
The Requirements Grow
A week later, somebody asks:
“Can we add disk usage?”
Then:
“Can we also collect operating system information?”
Then:
“Can we identify stale servers?”
Then:
“Can we generate reports?”
Before long we end up with multiple functions:
Get-ServerHealth
Get-ServerDisks
Get-ServerOS
Get-ServerServices
Get-ServerUpdates
Get-ServerCertificates
New-ServerHealthReport
These functions typically share information.
For example:
Get-ServerOS -ComputerName SRV01
and
Get-ServerDisks -ComputerName SRV01
both operate on the same thing. A server. This is the first sign we might be dealing with an object rather than a simple collection of data.
Identifying the Object
When introducing classes, the first question should always be:
“What am I modelling?”
In our case, a server. A server has attributes:
Name
Online
LastBootTime
OperatingSystem
Disks
And it has behaviour:
Check connectivity
Get OS information
Get disk information
Determine health
As we saw in part 1, those map directly to class members.
Creating the First Class
Let’s start small.
class Server {
[string]$Name
[bool]$Online
}
This is not particularly useful yet. We can create one like this:
$server = [Server]::new()
$server.Name = "SRV01"
$server.Online = $true
But we’re still manually populating properties. Let’s improve that.
Adding a Constructor
Constructors control how objects are created.
class Server {
[string]$Name
[bool]$Online
Server([string]$Name) {
$this.Name = $Name
}
}
Now object creation becomes more intentional:
$server = [Server]::new("SRV01")
The resulting object immediately represents something meaningful.
Giving the Object Behaviour
An object becomes interesting when it can perform actions. Let’s allow the server to determine whether it is online.
class Server {
[string]$Name
[bool]$Online
Server([string]$Name) {
$this.Name = $Name
}
[bool] TestConnection() {
$this.Online = Test-Connection -ComputerName $this.Name -Count 1 -Quiet
return $this.Online
}
}
Using it:
$server = [Server]::new("SRV01")
$server.TestConnection()
What changed? Instead of writing:
Test-Connection -ComputerName SRV01
all over the script, the object now understands how to test itself.
Retrieving Operating System Information
Let’s make the object smarter.
class Server {
[string]$Name
[bool]$Online
[string]$OperatingSystem
[datetime]$LastBootTime
Server([string]$Name) {
$this.Name = $Name
}
[void] Refresh() {
$this.Online = Test-Connection -ComputerName $this.Name -Count 1 -Quiet
if (-not $this.Online) {
return
}
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $this.Name
$this.OperatingSystem = $os.Caption
$this.LastBootTime = $os.LastBootUpTime
}
}
Usage becomes surprisingly clean:
$server = [Server]::new("SRV01")
$server.Refresh()
$server
Output:
Name : SRV01
Online : True
OperatingSystem : Windows Server 2022
LastBootTime : 01/07/2026 03:15:42
Notice how we are hiding complexity. The consumer does not need to know about:
- WMI
- CIM
- Win32_OperatingSystem
- Connection testing
The implementation details are encapsulated.
Working with Multiple Servers
The biggest advantage appears when processing collections.
$servers = @(
[Server]::new("SRV01"),
[Server]::new("SRV02"),
[Server]::new("SRV03")
)
Refresh all:
$servers | ForEach-Object {
$_.Refresh()
}
Display results:
$servers
Every object now contains both data and logic. This is fundamentally different from a PSCustomObject.
Making Objects Self-Aware
One feature I particularly appreciate in classes is calculated behaviour. For example:
class Server {
[datetime]$LastBootTime
[int] GetUptimeDays() {
return $((Get-Date) - $this.LastBootTime).Days
}
}
Usage:
$server.GetUptimeDays()
Instead of repeatedly writing throughout the script:
((Get-Date) - $server.LastBootTime).Days
The object becomes responsible for understanding itself.
Adding Simple Health Logic
Let’s add a basic health evaluation.
class Server {
[bool]$Online
[string] GetHealthStatus() {
if ($this.Online) {
return "Healthy"
}
return "Offline"
}
}
Usage:
$server.GetHealthStatus()
As requirements evolve, the internal logic can change without affecting the rest of the script. Today we only have: Online. Tomorrow we have: Online, Disk Space, Patch Status, Certificates and Services. With the class the consumer code stays exactly the same.
What Did We Actually Gain?
At this stage the code is arguably longer. That often surprises people. Classes are not primarily about reducing lines of code. They are about reducing complexity. We gained:
Encapsulation
Everything related to a server lives inside the server class.
Reusability
The same class can be used by:
- Reporting scripts
- Monitoring tools
- Scheduled jobs
- Dashboards
Discoverability
Modern editors understand class methods. Typing:
$server.
often reveals available functionality immediately.
Maintainability
Changes happen in one location instead of ten different functions.
What Did We Lose?
Let’s be honest. Classes introduce trade-offs. We lost some simplicity.
A quick report script may not benefit from:
class Server {}
at all. For example:
Get-ComputerInfo | Select-Object CsName, WindowsVersion
is still completely fine. Classes become valuable once your solution starts behaving more like a product than a script.
A Practical Rule
Whenever you see multiple functions operating on the same entity:
Get-ServerInfo
Get-ServerDisks
Get-ServerStatus
Get-ServerUptime
ask yourself:
“Would these make more sense as methods of a Server object?”
If the answer is yes, a class might improve your design.
Conclusion
Your first PowerShell class does not need inheritance, interfaces, abstract methods, or any advanced object-oriented concepts.
Start small, model a real-world thing, put its data and behaviour together. That’s it. A well-designed class often starts as nothing more than – class Server {} – and grows naturally as requirements evolve.
The goal is not to show off object-oriented programming. The goal is to make tomorrow’s maintenance easier than today’s. And that is where classes begin to earn their place in PowerShell.
Next in This Series: Part 3: Inheritance and Reuse
We’ll take our Server class and explore:
- Base classes
- Child classes
- Virtual machines
- Physical servers
- Cloud instances
- Shared functionality
We’ll also answer a question that causes many PowerShell developers headaches:
“Just because you can use inheritance, does that mean you should?”

Leave a Reply