I was recently reading an article that I’ll refrain from linking to for fear of a flame war. It infuriated me because it referred to PowerShell as a procedural/functional language that is antiquated and more like shell scripting. I reflected on this for a while and I came to the realization that if I were to look at 99.99999% of the code out there I may think the same thing. I also realized that there are reasons that PowerShell code appears procedural instead of object oriented.
This post will do a few things
- Show you how to do object-oriented programming with PowerShell using a traditional approach
- Show you the alternative (still object-based) so you can see why it’s generally used
A Class
Let’s describe a dog.
Properties
A dog has a name, color, and is some size.
$dogclass = new-object psobject -Property @{
color = $null
name = $null
size = $null
}
A Constructor
While it may not be needed because you can instantiate the object without doing so (via psobject.copy()), object-oriented developers love their constructors. It’s also a good place to add some validation for your class. For example, in the below constructor we’ll restrict the size to either small, medium, or large.
function DogClass {
param(
[Parameter(Mandatory=$true)]
[String]$name,
[Parameter(Mandatory=$false)]
[string]$color,
[Parameter(Mandatory=$false)]
[ValidateSet('Small','Medium','Large')]
[String]$size
)
$dog = $DogClass.psobject.copy()
$dog.name = $name
$dog.color = $color
$dog.size = $size
$dog
}
Now you can start using your class
08:31:50 PS C:\> $lucy = DogClass -name Lucy -color brown -size large
08:45:07 PS C:\> $lucy
color name size
----- ---- ----
brown Lucy large
A Method
A dog performs certain functions. For example, a dog is known to pee. We can modify our dog class with a method by using Add-Member. Also note that you can use the $this special variable to access the object that is invoking the method.
$DogClass |Add-Member -MemberType ScriptMethod -Name "Pee" -Value {
"A warm refreshing pee trickles out of {0}" -f $this.name
}
With the method created, you can instantiate Lucy again from the modified class and access her new method.
08:50:50 PS C:\> $lucy = DogClass -name Lucy -color brown -size large
08:52:30 PS C:\> $lucy.Pee()
A warm refreshing pee trickles out of Lucy
Accessor Functions
Accessor functions help you protect the properties of an object. As in Perl and Python, there is no real protected or private property available to you. Therefore, you can use the same convention that these other languages use to supply an underscore prefix to any private methods or properties. It doesn’t actually prevent people from using it, but it’s a clear sign that they shouldn’t use it. For example, if we wanted to make the size property private, we would modify the class to look like this. Note: I’m adding the scriptmethod we created in one step using the -PassThru parameter of Add-Member.
$DogClass = new-object psobject -Property @{
color = $null
name = $null
_size = $null
} |Add-Member -PassThru -MemberType ScriptMethod -Name "Pee" -Value {
"A warm refreshing pee trickles out of {0}" -f $this.name
}
ScriptMethod
With the new _size property, it’s easy enough to modify our constructor to use the new property, but what if you want to control how people set or get the property of size. You can create an accessor function to do this. Basically, this is just a method that will set or return the data from _size.
$dogclass |Add-Member -PassThru -MemberType ScriptMethod -Name Size -Value {
param(
[Parameter(Mandatory=$false, Position=0)]
$Size
)
if ($size) {
$this._size = $size
} else {
$this._size
}
}
Now, we can access the data in _size by using the accessor.
$lucy = DogClass -name Lucy -color brown -size large
"The dog is {0}" -f $lucy.Size()
We can also set the _size using the same function.
$lucy.Size('medium')
It’s important to note that I lost the property validator I have in the constructor. ScriptMethods break if you have an optional parameter that also has a validator. There are two ways to handle this. Either you add validation to the Size() method or you create two accessor functions, SetSize() and GetSize(). Both are acceptable and both are very easy to implement. Here’s an example of implementing your own validatior
ScriptProperty
A nice way to allow access to the pseudo private properties is to use the ScriptProperty member instead. This provides you with a way to perform a scripted action for a get, but it also allows you to do validation on a set. The difference is that it allows you to use the equal sign instead of a method to set the property.
Here’s what a ScriptProperty looks like. The first ScriptBlock is the get function and the second ScriptBlock is the set function.
$dogclass |Add-Member -MemberType ScriptProperty -name Size -Force -Value {
$this._size
} {
param(
$Size
)
if (@('small','medium','large') -contains $size) {
$this._size = $size
} else {
throw "This is not a valid size. A size must be small, medium, or large"
}
}
I’m using Force above to override the Size member I already created. In order to use the new property
In order to use this property, I can now do the following with my instantiated instance of Lucy.
PS C:\> $lucy.Size = 'blah'
Exception setting "Size": "This is not a valid size. A size must be small, medium, or large"
At line:1 char:1
+ $lucy.Size = 'blah'
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], SetValueInvocationException
+ FullyQualifiedErrorId : ScriptSetValueRuntimeException
PS C:\> $lucy.Size = 'medium'
PS C:\> $lucy.Size
medium
A nice thing about this technique is that your custom formatters will be able to see the property. In other words, it can be used in Select, Format-Table, Format-List, etc.
Class Inheritance
PowerShell doesn’t have real inheritance. However, the functionality of inheritance is very easily achieved. If you’re not familiar with inheritance, it’s pretty straightforward. Many times a class will have subclasses. For instance, a Scottish Terrier is a subclass of dog that is always small, black, and prefers to chase rats (rather than hunt like a beagle or keep your feet warm like a pug). Therefore, we can create a Scotty class that is based on the dog class, but has these properties set by default and has a method for hunting vermin.
Inherited Class
Because the definition of our class is simply an object, we can copy and modify it to create a new class that can be used later by other things.
$ScottyClass = $DogClass.psobject.copy()
$ScottyClass.color = 'black'
$ScottyClass._size = 'small'
One thing to note when creating an inherited class in PowerShell using this technique is that you need to recreate your constructors. This is probably the only unfortunate part of the pseudo-class inheritance that PowerShell offers. It basically forces override whether you want it or not on the constructor.
function New-Scotty {
param(
[Parameter(Mandatory=$true)]
[String]$name
)
$dog = $ScottyClass.psobject.copy()
$dog.name = $name
$dog
}
Now we can use our new class:
$George = New-Scotty George
Overriding Methods
In the case of the Scotty class where we want to add a new method for catching rats, there’s nothing special that needs to be done. New methods are treated the same as on the base class. However, if you want to override a method that exists on the parent class, the technique is identical, but you’ll need to supply the force parameter to Add-Member. For example, if we want to override the Pee() method for the dog, we could do the following.
$ScottyClass|Add-Member -Force -MemberType ScriptMethod -Name "Pee" -Value {
"The Scotty sniffs the tree. A warm refreshing pee trickles out of {0}" -f $this.name
}
Note about PIA 2.0
Bruce Payette has a great set of code in PowerShell in Action 2.0 about extending PowerShell to wrap the OO-ability of the language in something that looks cleaner. The end result is you can define a class like this:
DogClass {
property name
property size
method Pee { "Oh yeah"}
}
$dog = new DogClass
While doing research for this post, I realized that I had never noticed this section of the book. We really need to make this a publicly available extension. It’s pretty simple to do, but I don’t want to paste his code due to copyright reasons. Actually, if you’re the kind of person who has been looking for OO in PowerShell and have never read Bruce’s book, you probably should because it will answer a whole lot more about PowerShell than just this for you.
Objects from New-Module
The PowerShell team added a shortcut way to do all of the above. Modules exist in their own closure. Because of this, they may contain their own scoped variables that can be stored within the module itself. There is a parameter on New-Module called -AsCustomObject that will convert your defined scriptblock module into a PowerShell object with note properties and script methods. Here’s an example of the Dog class using this technique.
function New-Dog {
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$false)]
[ValidateSet('small', 'medium','large', $null)]
[string]$size,
[Parameter(Mandatory=$false)]
[string]$color
)
New-Module -Argumentlist @($name,$size,$color) -AsCustomObject {
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$false)]
[ValidateSet('small', 'medium','large', $null)]
[string]$size,
[Parameter(Mandatory=$false)]
[string]$color
)
function Pee {
"A warm refreshing pee trickles out of {0}" -f $Name
}
Export-ModuleMember -Function Pee -Variable Name, Size, Color
}
}
If we look at the members that this creates, you’ll see it looks very similar to what we’ve been doing up until now.
$Lucy = New-Dog -Name Lucy
$Lucy |Get-Member
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
color NoteProperty System.String color=
Name NoteProperty System.String Name=blah
size NoteProperty System.String size=
Pee ScriptMethod System.Object Pee();
The PowerShell Way
So what we’ve been looking at is modern object oriented programming. However, it’s rarely seen in the wild with PowerShell even though it’s very simple to implement. Why is that exactly? Sure, there are plenty of beginners out there who weren’t even developers until now, but I believe it’s deeper than that. I personally know that this exists. However, I’ve only used it on one project.
The reality is that PowerShell lives and breaths objects through everything. Also, these objects are very flexible because they can be extended or modified on the fly. Functions can take objects as input and the type of Input provided by strongly typed objects can more easily be achieved with strongly type parameters. I see the param() block of a function as something that describes the properties of an object that I expect to see.
Now this is PowerShell to me. This is a paradigm shift in the way we do object-oriented programming. To me, when my peers ask me why I’m so PowerShell crazy, this is the answer. It looks like it’s functional, but it is far from that.
function New-Dog {
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$false)]
[ValidateSet('small', 'medium','large', $null)]
[string]$size,
[Parameter(Mandatory=$false)]
[string]$color
)
New-Object psobject -property @{
Name = $Name
Size = $Size
Color = $color
}
}
function Invoke-Pee {
param(
[Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
[String]$Name
)
PROCESS {
"A warm refreshing pee trickles out of {0}" -f $Name
}
}
New-Dog -Name Lucy |Invoke-Pee
A warm refreshing pee trickles out of Lucy
Not only do I have a dog that can pee, but I also can use the same Invoke-Pee function for anything that has a name property. It can also invoke-pee on a number of dogs without having to create a loop. If I really needed to be sure it was a dog, I could take the entire psobject as a parameter and validate that it had the properties that make it a dog. Perhaps even write the Test-Dog function to do that.
Finally, I would personally wrap the above in a module named Dog and use a prefix more like New-Dog and Invoke-DogPee. Then it’s easy to import the module and inspect what methods are available using Get-Command -Module Dog. Again, this looks and smells like it’s functional, but it’s all objects and encapsulation, and it’s amazing!