Pseudo-Interfaces in Powershell

on Monday, January 28, 2019

In the last post I griped a bit about classes in Powershell. The strongest case to actually have classes in Powershell is that they can serve as interfaces which developers can use to extend modules and improve unit testing. Interfaces are a powerful part of DI/IoC and that pattern is not well supported in most command line scripting languages.

Here’s a quick run down of some of the techniques that can be used to provide a Pseudo-Interface in Powershell:

  • Powershell Classes
    • Example Use Case: I can’t really think of any off the top of my head.
    • Benefits: I’m not a fan of this approach.
    • Downsides: Since Powershell classes can’t be dynamically updated in the runtime, you have to continually reload your runtime environment whenever you want to make a change to one. This breaks one of the best benefits of using Powershell, and makes the language less dynamic.
    • How it works: This is built into powershell.
  • Function Overrides in Limited Scopes
    • Example Use Case: Pester
    • Benefits: This allows for the creation of Mock objects and limited scope overrides to common functions. This makes it possible to do unit testing of your Powershell modules that similar (but not exactly like) using Dependency Injection. This also keeps all of the coding to implement the pattern within powershell, so it can be dynamically updated.
    • Downsides: Since it’s building a Mock functions using Powershell dynamic creation behavior, the mock objects can be passed around as if they were created by a Dependency Injection system; but you can only have one definition for a function in each scope. You can’t have two functions which implement a common interface available within the same scope.
    • How it works: I don’t know. Pester is magic.
  • Function Aliases
    • Example Use Case: PoShDynDnsApi
    • Benefits: This allows for all your code to be rewritten against a standard alias, which will always work as expected; but still be flexible enough to handle difference of runtime environments (Windows vs Linux). This also keeps all of the coding to implement the pattern within powershell, so it can be dynamically updated.
    • Downsides: Similar to Function Overrides, you can only have a single definition for the Alias at a time. You can’t have two functions which implement a common interface available at the same time.
    • How it works: During a module’s load (the execution of the .psm1 file), the module can test the capabilities of the environment and determine what concrete implementation the alias should be set to.
  • Submodules
    • Example Use Case: Below
    • Benefits: Allows for multiple implementations of an interfaces to be available at the same time. This also keeps all of the coding to implement the pattern within powershell, so it can be dynamically updated.
    • Downsides: Your code will be unable to use object piping when calling functions using this pattern. And, it’s a lot more work to implement since intellisense won’t be able to help you.
    • How it works: It uses Invoke-Expression to dynamically call different implementations of a function. Example below.

Submodule Pseudo-Interface Pattern

A module which uses submodules might look something like this:

image

In the picture above, the Notifications module contains multiple submodules, but the 3 that are expanded in the directory structure are NSConsole, NSEmail, and NSSlack. Each of these modules expose two public functions which the parent module can use: Send-NotificationToSubscription and Initialize-Module.

They all implement the same function definition for Send-NotificationToSubscription. For example, here’s NSConsole’s:

The submodule, NSConsole, is called from Notifications’ Send-Notification function. The Send-Notification calls the submodule dynamically, which makes it act very similarly to an interface:

You can see within Send-Notification code that the pattern is actually used 3 times:

  • "$($global:Notifications.AMModule)\Save-Notification": This calls the Auditing Manager for the system to record the notification within the audit log. There are implementations for the production code and for local development. This code also be implemented using Function Overrides or Aliases.
  • "$($global:Notifications.CMModule)\Select-MatchingSubscription": This calls the Configuration Manager subsystem to retrieve subscription which would match the notification. There are implementations for the production code and for local development. This code also be implemented using Function Overrides or Aliases.
  • "$senderModule\Send-NotificationToSubscription": This can dynamically call different implementations of the Notifications Senders (Console, Email, or Slack). Since there are multiple implementations of the same interface it can only be done using the submodule pattern.

One final note. You should load the submodules which you intend to use at the time the top-level module (Notifications) is loaded. You can dynamically load them later, but it’s much easier to just load them when the top-level module is loaded.

Here’s what loading a module would look like:

And, Load-NotificationSender ($root is defined in the .psm1 example above):

1 comments:

Anonymous said...

This is a wildly under-appreciated post. Well done.

Post a Comment


Creative Commons License
This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.