Abstract
The release of Powershell WMF5 added classes to help simplify the creation of DSC resources. The class wrapper helps us encapsulate and localize variables and methods by creating objects. Classes expose an entirely new paradigm to Powershell known as Object-Oriented Programming. Taking a glance at the authoring process for earlier modules and DSC resources we can see a few common problems. Code duplication, an easily identifiable code smell, is found through many of the open source projects. Dynamic scoping and encapsulation issues forced developers to clear, cast, or rename variables. Many of these problems are easily resolved using OOP. The following article outlines the basic syntax of PowerShell classes while subsequently introducing intermediate and advanced design pattern concepts.
Introduction
Developers new to PowerShell, often find themselves looking for better design patterns to keep code clean and manageable.
Since PowerShell is a functional language, it felt worthwhile to experiment with modern design patterns commonly seen in languages like JavaScript.
One example of this is closures
. Keep in mind, treating PowerShell like JavaScript is a recipe for a hard time – albeit an entertaining challenge.
Eventually, we find ourselves settling for the current arsenal such as Splatting, Pipelines, and Remoting. The introduction of class-based resources rekindles the quest for better code design. At that time, finding resources on the new syntax was nearly impossible.
Huge thanks go out to June Blender for her section on MVA (What’s new in Powershell v5) as well as her blog articles over at Sapien. June’s contributions helped us get a handle on the basics and inspired me to help others in return.
- For anyone primarily looking for information on authoring DSC Class based resources.
- Matthew Hodgkins has an excellent article to get you started.
- For testing WMF4/5 resources, I highly recommend investing time with
Test-Kitchen
andKitchen-DSC
.- Steven Murawski from Chef provides an exceptional collection of resources.
- Class-based resouces requires some workarounds.
- Doug Finke and Christopher Hunt provide a few articles on the caveats.
Syntax Overview
Class Structure
The following is an overview of Object-Oriented Programming and Class syntax in WMF5.
Classes
We can think of Classes
as models or blueprints.
To use a Class, we create a special Type of variable known as an Object; an Object is an instance of a Class.
Think of a class as a way to create a specification of variables, functions, and other properties (e.g. a template). Now to use this model, we must create an instance of the specification (object). Similarly, this is the same idea where a contractor can use a blueprint to build multiple houses. Once created, an object has access to properties and methods defined by its class.
1 2 3 4 | # Class Syntax class CyberNinja { } |
Properties
Properties
are a special type of class member which define a field (data variable) as well as hidden methods to get and set the value.
A Property is composed of a data type
, name
, default value
, access modifier
, and non-access modifier
.
- Data Type: A data type can be a built-in type like [String], [Int32], [Bool].
- Additionally, this could also be a custom data type such as another PowerShell class:
[CyberNinja]
.
- Additionally, this could also be a custom data type such as another PowerShell class:
- Name: The name of the property.
- The name follows a set of allowed rules such as alphanumeric, underscores, dashes, and, numbers.
- Default Value (Optional): Specifies the default value of a Property when creating an object.
- If a value is not declared, the property will not always be null.
- The default value of the properties data type determines the value.
- Access Modifier (Optional): [public, hidden]. The default modifier is public; however, we do not use the public keyword.
- Non-access Modifier (Optional): The static keyword controls if a property is an instance or class/static type.
1 2 3 4 5 6 7 8 9 | # Public Properties [String] $Alias [Int32] $HitPoints # Static Properties static [String] $Clan = "Posh Shinobi" # Hidden Properties hidden [String] $RealName |
Note: More on Access Modifiers below.
Methods
The term Method
is a fancy way of describing a function defined inside of a class.
In OOP, a method can take arguments the same as a function; however, they must return a value.
- If a method does not return a value, the return type is
[Void]
. - A data type should be type-hinted for each argument in the method header, e.g.,
[String] $Name
.
A method should have an access modifier
, name
, arguments
, and return type
.
- The access modifier is considered public if left blank.
- If
static
is not declared the property will be an instance type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # Instance Method [String] getAlias() { return $this.Alias } # Static Method static [String] getClan() { return [CyberNinja]::Clan } # Static Method static [String] Whisper ([String] $Name) { return "Hello {0}!" -f $Name } |
$this
The $this
variable describes the current instance of the object. It is thought of like $_
for classes.
- If a property is not static, the syntax
$this.PropertyName
is used to reference the instance property. - To refer to methods simply use the method name or
$this.MethodName()
.
Note: A static method cannot use $this.
Constructor
A Constructor
is a type of method that is called only when an object is created.
- A constructor must use the same name as the class.
Let’s say we create a class called CyberNinja
with properties for the ninja’s Alias and HitPoints.
By design, we would not want to allow someone to create a ninja object without filling in the required properties.
To force any required arguments, we need a constructor.
Constructors are similar to the Begin
block in Functions
.
1 2 3 4 5 6 7 8 9 | class CyberNinja { # Constructor CyberNinja ([String] $Alias, [int32] $HitPoints) { $this.Alias = $Alias $this.HitPoints = $HitPoints } } |
ToString
ToString
is one of the convenient object methods seen in traditional OOP.
If an object is passed to a function which accepts a string argument, ToString will automatically be called.
- If ToString is not added/overwritten in the class, the default ToString method returns the class name.
- The default object behavior can be forced by casting the object to
[System.Object]
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class myColor { [String] $Color [String] $Hex myColor([String] $Color, [String] $Hex) { $this.Color = $Color $this.Hex = $Hex } [String] ToString() { return $this.Color + ":" + $this.Hex } } |
1 2 3 4 5 6 7 | PS C:\> $red = [myColor]::new("Red", "#FF0000") PS C:\> Write-Host $red Red:#FF0000 PS C:\> Write-Host ([System.Object]$red).ToString() myColor |
Example 1: Class Structure
The following code is a simple example of a basic class. Next, we look at how to use a class and continue with some more advanced concepts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class CyberNinja { # Properties [String] $Alias [int32] $HitPoints # Static Properties static [String] $Clan = "DevOps Library" # Hidden Properties hidden [String] $RealName # Parameterless Constructor CyberNinja () { } # Constructor CyberNinja ([String] $Alias, [int32] $HitPoints) { $this.Alias = $Alias $this.HitPoints = $HitPoints } # Method [String] getAlias() { return $this.Alias } # Static Method static [String] getClan() { return [CyberNinja]::Clan } # ToString Method [String] ToString() { return $this.Alias + ":" + $this.HitPoints } } |
Creating instances of a class
To use a class, we must instantiate an object unless using static properties or methods.
Most commonly, this is done using the new()
static method or the New-Object Command.
In some cases, such as creating classes dynamically by type, the New-Object command is necessary.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # Using Static "new" method. $Ken = [CyberNinja]::new("Ken", 28) # Using New-Object. Parameters for Argument list are positional and required by the constructor. $Hodge = New-Object CyberNinja -ArgumentList "Hodge", 31 # Using a HashTable. Note: requires default or parameterless constructor. $June = [CyberNinja]@{ Alias = "June"; HitPoints = 40; } # Dynamic Object Type using a variable name. $Type = "CyberNinja" $Steven = New-Object -TypeName $Type |
When considering code design, it is common to declare the object type explicitly.
1 | PS C:\> [CyberNinja] $Ken = [CyberNinja]::new("Ken", 28) |
Static vs. Instance
The keyword static
is a non-access modifier for properties and methods within a class.
- For properties, the value is the same across every instance of the class.
- For methods, the method cannot use instanced variables such as
$this.Name
Looking back at Example 1, if we create ten CyberNinja objects and change the $Clan property of any one of them, the change will be reflected in every single Object.
Static
- Uses the
::
operator to access the property or method. - Typically called using
[Class]::Property
, where “Class” is the name of the class.
1 2 3 4 5 6 7 8 9 10 11 12 13 | [CyberNinja] $Ken = [CyberNinja]::new("Ken", 28) # Call a Static Method $Ken::getClan() [CyberNinja]::getClan() # Fetch Static Prop Value $Ken::Clan [CyberNinja]::Clan # Set Static Prop Value $Ken::Clan = "DevOps Library" [CyberNinja]::Clan = "DevOps Library" |
Instance
- Uses the
.
operator to access the property or method. - Must be called on an instanced object of a class – not directly.
1 2 3 4 5 6 7 8 9 10 | [CyberNinja] $Ken = [CyberNinja]::new("Ken", 28) # Call an Instance Method $Ken.getAlias() # Fetch Instance Prop Value $Ken.HitPoints # Set Instance Prop Value $Ken.Alias = "Mekuto" |
Accessors/Mutators aka Getter/Setter
Properties in a PowerShell class can be static
, public
, and hidden
.
- By default, a property is public.
- To get a property value from an object, use
$obj.Name
. - To set a property from an object, use
$obj.Name = "New Name"
.
Powershell automatically creates accessor methods for getting and setting the value.
These hidden accessor methods are called get_x
, set_x
where “x” is the property name.
Hidden is not the same as private in languages such as C#.
If we create an object of the [CyberNinja] class and view the members with Get-Member
, we can see all of the public members.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | PS C:\> $Chris = [CyberNinja]::new("Mirishikiari", 28) PS C:\> $Chris | Get-Member PS C:\> $Chris | Get-Member TypeName: CyberNinja Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() getAlias Method string getAlias() GetType Method type GetType() ToString Method string ToString() Alias Property string Alias {get;set;} HitPoints Property int HitPoints {get;set;} |
Hidden members can be viewed by supplying the -Force
Flag to Get-Member
.
Note: V5 does not have private variables since PowerShell uses PowerShell for debugging.
In the following code example, notice the get_x and set_x methods PowerShell automatically creates for our properties. Our hidden RealName Property is also accessible – how quaint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | PS C:\> $Mike = [CyberNinja]::new("Xainey", 28) PS C:\> $Mike | Get-Member -Force TypeName: CyberNinja Name MemberType Definition ---- ---------- ---------- pstypenames CodeProperty System.Collections.ObjectModel.Collection`1... psadapted MemberSet psadapted {Alias, HitPoints, get_Alias, set... psbase MemberSet psbase {Alias, HitPoints, get_Alias, set_Al... psextended MemberSet psextended {} psobject MemberSet psobject {Members, Properties, Methods, Imm... Equals Method bool Equals(System.Object obj) getAlias Method string getAlias() GetHashCode Method int GetHashCode() GetType Method type GetType() get_Alias Method string get_Alias() get_HitPoints Method int get_HitPoints() get_RealName Method string get_RealName() set_Alias Method void set_Alias(string ) set_HitPoints Method void set_HitPoints(int ) set_RealName Method void set_RealName(string ) ToString Method string ToString() Alias Property string Alias {get;set;} HitPoints Property int HitPoints {get;set;} RealName Property string RealName {get;set;} |
Hidden properties are masked, but still accessible to the debugger. June Blender - @juneb
Get-Member
can be useful for finding methods on custom objects and built-in objects.
1 2 3 4 5 6 7 8 | # All [Math] | Get-Member # Static Only Methods [Math] | Get-Member -Static # Hidden [Math] | Get-Member -Force |
Overloaded Methods
Method Overloading
is a way to define multiple methods with the same name.
Overloaded methods behave differently depending on the number of arguments or the data types of the arguments supplied.
In the following code example, SayHello()
and add()
can be called different ways.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class OverloadExample { static [String] SayHello () { return "Hello There!" } static [String] SayHello ([String] $Name) { return "Hello {0}!" -f $Name } static [int] add([int] $a, [int] $b) { return $a + $b } static [double] add([double] $a, [double] $b) { return $a + $b } } |
1 2 3 4 5 6 7 8 9 10 11 | PS C:\> [OverloadExample]::SayHello() Hello There! PS C:\> [OverloadExample]::SayHello("Mike") Hello Mike! PS C:\> [OverloadExample]::add(1, 2) 3 PS C:\> [OverloadExample]::add(1.1, 2.3) 3.4 |
Methods and Constructors are overloadable.
We could also refactor the above example to simplify SayHello()
.
In this next snippet, the parameterless SayHello()
method is funneled through the single argument method.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class OverloadRefactor { # Calls Overloaded Method static [String] SayHello () { return [OverloadRefactor]::SayHello("There") } static [String] SayHello ([String] $Name) { return "Hello {0}!" -f $Name } } |
1 2 3 4 5 | PS C:\> [OverloadRefactor]::SayHello() Hello There! PS C:\> [OverloadRefactor]::SayHello("Mike") Hello Mike! |
Inheritance
Inheritance
allows for programmers to create classes from existing classes by extending them.
In this way, we can reuse classes and extend the functionality without editing a closed class. *cough SOLID*
When a class is extended, all of the members from the base or parent class are inherited (passed on) to the child class.
- To extend a class, use the syntax
Class Child : Parent
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # Foo is the parent class class Foo { [string] $Message = "Hello!" [string] GetMessage() { return ("Message: {0}" -f $this.Message) } } # Bar extends Foo and inherits its members class Bar : Foo { } |
The class Bar does not declare any properties or methods. If we create an instance of the Bar class, it will inherit all of the members of its parent class.
1 2 3 4 5 6 7 | PS C:\> $myBar = [Bar]::new() PS C:\> $myBar.Message Hello! PS C:\> $myBar.GetMessage() Message: Hello! |
Note: To override this behavior, we must redeclare the members in the child class.
Using the Base Constructor
A child class can call the constructor of its parent by using the : base()
command on its constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class ZeroWing { [String] $User [String] $Message ZeroWing([String] $User, [String] $Message) { $this.User = $User $this.Message = $Message } [String] TurnOn() { return ("{0} : {1}" -f $this.User, $this.Message) } } class MainScreen : ZeroWing { MainScreen([String] $User, [String] $Message) : base($User, $Message) { } } |
The constructor for MainScreen
maps parameters to the base constructor.
1 2 3 4 5 6 7 | PS C:\> $mainScreen = [MainScreen]::new("CATS", "All your base are belong to us.") PS C:\> $mainScreen.User CATS PS C:\> $mainScreen.TurnOn() CATS : All your base are belong to us. |
Enumerations
An Enum
is a special Type which defines a set of named constants.
In PowerShell, we can use an Enum as an argument type for a method in a Class.
The Enum type lets a method restrict the argument values it can accept.
1 2 3 4 5 6 7 | Enum Turtles { Donatello Leonardo Michelangelo Raphael } |
1 2 3 4 5 6 7 | Enum Turtles { Donatello = 1 Leonardo = 2 Michelangelo = 3 Raphael = 4 } |
1 2 3 4 5 | To get names from an enum PS C:\> [System.Enum]::GetValues([Turtles]) To get int values from an enum PS C:\> [System.Enum]::GetValues([Turtles]) | foreach { [int] $_ } |
Design Patterns
Polymorphism
Polymorphism
commonly uses a parent class to reference a child class.
In more advanced cases we rely on interfaces. However, interfaces are not included natively in PowerShell v5.
Using classes to demonstrate polymorphism is straightforward as seen in the next example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | class Foo { [string] $SomePram Foo([string]$somePram) { $this.SomePram = $somePram } [string] GetMessage() { return $null } [void] WriteMessage() { Write-Host($this.GetMessage()) } } class Bar : Foo { Bar([string]$somePram): base($somePram) { } [string] GetMessage() { return ("{0} Success" -f $this.SomePram) } } class Bar2 : Foo { Bar2([string]$somePram): base($somePram) { } [string] GetMessage() { return ("{0} Success" -f $this.SomePram) } } [Foo[]] $foos = @([Bar]::new("Bar"), [Bar2]::new("Bar2")) foreach($foo in $foos) { $foo.WriteMessage() } |
1 2 | Bar Success Bar2 Success |
While this may work in some cases, we may decide that the logic for WriteMessage()
should not be in the base class.
If we had interfaces, this feat would be simple enough.
Since we do not, this brings us to the next pattern: Abstract Classes.
Abstract Classes
An Abstract Class
is similar to a combination of an Interface and a Class.
It can be used to define the underlying contract required by any class extending it.
- In OOP a class which extends an abstract class should implement all of the defined methods, similar to an interface.
- Additionally, we cannot create an instance of an abstract class.
Since PowerShell does not have the abstract
keyword, we need to simulate this functionality. In the next example:
- Foo cannot be directly instantiated
SayHello()
in the Foo class must be overridden by a child class. Otherwise, we should throw an error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Foo { Foo () { $type = $this.GetType() if ($type -eq [Foo]) { throw("Class $type must be inherited") } } [string] SayHello() { throw("Must Override Method") } } class Bar : Foo { Bar () { } [string] SayHello() { return "Hello" } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # Can not instantiate [Foo] # $Foo = [Foo]::new() # Since we cannot create a direct object from [Foo] we cannot call SayHello() # $Foo.SayHello() # [Bar] must override SayHello() $Bar = [Bar]::new() $Bar.SayHello() # Can use polymorphism [Foo[]] $MyFoo = @([Bar]::new()) foreach($obj in $MyFoo) { $obj.SayHello() } |
Singleton
The Singleton
design pattern used to restrict the instantiation of a Class to one object only.
Many other design patterns require a singleton implementation.
Typically, we want to use a singleton when:
- There should only be a single global object.
- There is a need to control concurrent access to a shared resource.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Singleton { # Instanced Property [int] $SomeParm static [Singleton] $instance static [Singleton] GetInstance() { if ([Singleton]::instance -eq $null) { [Singleton]::instance = [Singleton]::new() } return [Singleton]::instance } } |
1 2 3 4 5 6 7 8 9 10 11 | PS C:\> $single = [Singleton]::GetInstance() PS C:\> $single.SomeParm = "Highlander" PS C:\> $single.SomeParm Highlander PS C:\> $another = [Singleton]::GetInstance() PS C:\> $another.SomeParm Highlander |
There can be only one!
Factory Pattern
The Factory Pattern
considered a creational pattern, is by far one of the most valuable models.
It lets us create objects, though a common factory interface, without needing to reference an exact class.
This pattern also allows for the creation of objects without exposing the underlying creation logic.
Note: The following example builds on my earlier concepts on abstract classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | <# # Abstract Class: Drink # Create an Abstract drink class to serve as the interface. # Constructor restricts this class being instantiated directly. # Methods are defined and force children to override. #> class Drink { [String] $Name [Int32] $Caffeine Drink ([String] $Name, [Int32] $Caffeine) { $type = $this.GetType() if ($type -eq [Drink]) { throw("Class $type must be inherited") } $this.Name = $Name $this.Caffeine = $Caffeine } [string] Open() { throw("Must Override Method") } } <# # EnergyDrink Class: implements Drink # Constructor uses base constructor. # Methods overrides are declared. #> class EneryDrink : Drink { EneryDrink ([String] $Name, [Int32] $Caffeine) : base ($Name, $Caffeine) { } # @Override [string] Open() { return "Popped the tab on a can of: {0}" -f $this.Name } } <# # Soda Class: implements Drink # Constructor uses base constructor. # Methods overrides are declared. #> class Soda : Drink { Soda ([String] $Name, [Int32] $Caffeine) : base ($Name, $Caffeine) { } # @Override [string] Open() { return "Twisted the top of a bottle of: {0}" -f $this.Name } } <# # Factory Class: DrinkFactory # Instance Methods generate new Drinks. # Static Properties/Methods demonstrate Storage/Fetch Concepts. #> class DrinkFactory { #Store and Fetch static [Drink[]] $Drinks static [Object] getByType([Object] $O) { return [DrinkFactory]::Drinks.Where({$_ -is $O}) } static [Object] getByName([String] $Name) { return [DrinkFactory]::Drinks.Where({$_.Name -eq $Name}) } #Create an instance [Drink] makeDrink([String] $Name, [String] $Caffeine, [String] $Type) { return (New-Object -TypeName "$Type" -ArgumentList $Name, $Caffeine) } } |
Standard Factory Object Generation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | PS C:\> [DrinkFactory] $DrinkFactory = [DrinkFactory]::new() PS C:\> [Drink] $Beverage1 = $DrinkFactory.makeDrink("RedBull", 100, "EneryDrink") PS C:\> [Drink] $Beverage2 = $DrinkFactory.makeDrink("Monster", 100, "EneryDrink") PS C:\> [Drink] $Beverage3 = $DrinkFactory.makeDrink("Coke", 100, "Soda") PS C:\> $Beverage1.Open() Popped the tab on a can of: RedBull PS C:\> $Beverage2.Open() Popped the tab on a can of: Monster PS C:\> $Beverage3.Open() Twisted the top of a bottle of: Coke |
Using Static Methods to Set/Fetch Objects
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | PS C:\> [DrinkFactory]::Drinks = @( [EneryDrink]::new("RedBull", 28), [EneryDrink]::new("Monster", 20), [Soda]::new("Coke", 24) ) PS C:\> [DrinkFactory]::getByType([EneryDrink]) Name Caffeine ---- -------- RedBull 28 Monster 20 PS C:\> [DrinkFactory]::getByName("Coke") Name Caffeine ---- -------- Coke 24 |
Method Chaining
Method chaining
is a popular design pattern in languages such as JavaScript and PHP.
Traditionally, in functional languages, functions or constructors required large numbers of positional arguments.
Method chaining gives us a way to create an object and set these values in a maintainable and readable fashion.
This pattern is also known as the named parameter idiom. To create this pattern:
- Create a method to set a property.
- Set the return type of the method to the class type.
- Set an instance variable.
- Return
$this
.
Note: The following example uses Enums to handle defaults easily.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | Enum Crust { Thin HandTossed DeepDish } Enum Sauce { Marinara GarlicParmesan Buffalo } Enum Toppings { Pepperoni Sausage Chicken } class Pizza { [Crust] $crust [Sauce] $sauce [Toppings] $toppings # Default, Parameterless Constructor Pizza() { } # Named Constructor static [Pizza] newOrder() { return [Pizza]::New() } [Pizza] chooseCrust([Crust] $crust) { $this.crust = $crust return $this } [Pizza] addSauce([Sauce] $sauce) { $this.sauce = $sauce return $this } [Pizza] addToppings([Toppings] $toppings) { $this.toppings = $toppings return $this } [Void] placeOrder() { Write-Host ("Pizza ordered. Details {0}" -f $this.toString()) } [String] toString() { return "Crust: {0} Sauce: {1} Toppings: {2}" -f $this.crust, $this.sauce, $this.toppings } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # separate steps $myPizza = [Pizza]::new() $myPizza.chooseCrust("DeepDish").addSauce("GarlicParmesan").addToppings("Sausage") | Out-Null $myPizza.placeOrder() # combined steps, using named constructor [Pizza]::newOrder().chooseCrust("HandTossed").addSauce("Marinara").addToppings("Pepperoni").placeOrder() # Using Normal Constructor: Parens not required in first example ([Pizza]::new()).placeOrder() (New-Object -TypeName Pizza).placeOrder() # Chaining Example Multiline. The "." on the right feels strange. cls [Pizza]::newOrder(). chooseCrust("HandTossed"). addSauce("Marinara"). addToppings("Pepperoni"). placeOrder() # Chaining Example 2 Multiline [Pizza]::newOrder() ` | %{$_.chooseCrust("HandTossed")} ` | %{$_.addSauce("Marinara")} ` | %{$_.addToppings("Chicken")} ` | %{$_.placeOrder()} |
Base Splat Pattern
While this may not be a traditional class design pattern, I created this pattern to simply using object properties with other PowerShell commands.
Calling a function with many positional arguments OR many named Parameters can become difficult to manage.
To solve this issue, we use a technique known as Splatting.
Using object properties to Splat a function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Concept { [String] $Name [Int32] $Answer Concept ([String] $Name, [Int32] $Answer) { $this.Name = $Name $this.Answer = $Answer } } function Get-Meaning ($Name, $Answer) { "The meaning of {0} is {1}." -f $Name, $Answer } [Concept] $concept = [Concept]::new("Life", 42) $parms = @{ Name = $concept.Name Answer = $concept.Answer } Get-Meaning @parms |
Using Base Helper to Generate Splat HashTable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class Helper { [HashTable] Splat([String[]] $Properties) { $splat = @{} foreach($prop in $Properties) { if($this.GetType().GetProperty($prop)) { $splat.Add($prop, $this.$prop) } } return $splat } } class Concept : Helper { [String] $Name [Int32] $Answer [Boolean] $HasTowel Concept ([String] $Name, [Int32] $Answer) { $this.Name = $Name $this.Answer = $Answer } } function Get-Meaning ($Name, $Answer) { "The meaning of {0} is {1}." -f $Name, $Answer } |
Using the Helper
class, anytime we need to Splat selected properties from an object, the base class handles the heavy lifting.
1 2 3 4 5 6 | PS C:\> [Concept] $concept = [Concept]::new("Life", 42) PS C:\> $splat = $concept.Splat(("Name", "Answer")) PS C:\> Get-Meaning @splat The meaning of Life is 42. |
Loading Class Files
Currently, there are some restrictions for loading class files.
- Parent classes must load before their children.
Thankfully, classes and child classes do not have to be in the same file.
If we use the following common example of dot source loading .ps1
files, we could run into this restriction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue ) # Dot source the files foreach($import in $Public) { try { . $import.fullname } catch { Write-Error -Message "Failed to import function $($import.fullname): $_" } } Export-ModuleMember -Function $Public.Basename |
Possible Workarounds
- Class/child class naming conventions which force alphabetic ordering precedence.
- Recursive try/catch until all of the classes load.
- Manually define the order and files in the module manifest.
- Use a lower level directory structure for child classes.
Conclusion
The PowerShell language is steadily evolving, which in turn adapts developer methodologies. People are amazing at taking a great invention and using it for entirely new ideas. Perhaps this is one of the reasons open source thrives. Take Play-Doh for example, Noah McVicker invented this goop as a wallpaper cleaner in the 1930s, around 20 years later it became a children’s toy. Hopefully, this article helps others learn PowerShell class syntax, OOP concepts, design patterns and explore new ideas.
Feel free to ask questions or send a pull request if you find anything that needs improvement.
References
- Five Tips for Writing DSC Resources.
- What’s new in PowerShell Version 5.
- Inheritance in PowerShell Classes
- Enumerations in PowerShell Classes
- Scripting Guys: PowerShell 5 create a simple class
- New-Object Command
- Testing PowerShell Classes
- Getting Started With Test-Kitchen and DSC
- Development in a Blink