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

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