2

I have read in various places that Global variables are at best a code smell, and best avoided. At the moment I am working on refactoring a big function based PS script to classes, and thought to use a Singleton. The use case being a large data structure that will need to be referenced from a lot of different classes and modules. Then I found this, which seems to suggest that Singletons are a bad idea too.

So, what IS the right way (in PS 5.1) to create a single data structure that needs to be referenced by a lot of classes, and modified by some of them? Likely pertinent is the fact that I do NOT need this to be thread safe. By definition the queue will be processed in a very linear fashion.

FWIW, I got to the referenced link looking for information on singletons and inheritance, since my singleton is simply one of a number of classes with very similar behavior, where I start with the singleton which contains collections of the next class, which each contain collections of the next class, to create a hierarchical queue. I wanted to have a base class that handled all the common queue management then extend that for the differing functionality lof each class. Which works great other than having that first extended class be a singleton. That seems to be impossible, correct?

EDIT: Alternatively, is it possible with this nested classes in a generic list property approach to be able to identify the parent from within a child? This is how I handled this is the Function based version. A global [XML] variable formed the data structure, and I could step through that structure, using .SelectNode() to populate a variable to pass to the next function down, and using .Parent to get information from higher up, and especially from the root of the data structure.

EDIT: Since I seem not to be able to paste code here right now, I have some code on GitHub. The example here of where the Singleton comes in is at line 121, where I need to verify if there are any other examples of the same task that have not yet comnepelted, so I can skip all but the last instance. This is a proof of concept for deleting common components of various Autodesk software, which is managed in a very ad hoc manner. So I want to be able to install any mix of programs (packages) and uninstall on any schedule, and ensure that the last package that has a shared component uninstall is the one that uninstalls it. So as to no break other dependent programs before that last uninstall happens. Hopefully that makes sense. Autodesk installs are a fustercluck of misery. If you don't have to deal with them, consider yourself lucky. :)

Gordon
  • 6,257
  • 6
  • 36
  • 89
  • 1
    It sounds like you _don't_ need a singleton - just create a single instance of your class and operate on that. In any case, your question is almost impossible to give a reasonable answer to - where is the existing code, and _what are you hoping to achieve_ by rewriting using classes? – Mathias R. Jessen May 26 '20 at 11:05
  • @mathias-r-jessen, I tried posting some code, but it seems like StackOverflow has a bug at the moment. When I paste the code it it automatically gets identified as code, but only partially formatted. It's enough code it's problematic. – Gordon May 26 '20 at 11:59
  • Regarding the refactor, it's mostly about code reuse. At the task level I have 30 or so different tasks, and a lot of duplicate code in each one. OOP solves that and other problems. Another place I am looking at a singleton is some initialization and status data. I need to keep track of the status (changes, no changes, warnings, errors) so I can rename the final log file based on the final status. Plus there are a number of properties I set at initialization and reference later. A Global Variable/Singleton Class seem like the only way to do that, other than constantly passing on that info. – Gordon May 26 '20 at 12:03
  • @mathias-r-jessen, got something hosted on Git Hub now. Formatting isn't great, but better than what I am getting here at the moment. – Gordon May 26 '20 at 12:23
  • I see _nothing_ in your code that requires implementation of a singleton, so I guess the answer is: _stop worrying about it_ :) – Mathias R. Jessen May 26 '20 at 12:37
  • @mathias-r-jessen So, how would I access the ProcessQueue so I can loop through and check for not yet completed tasks, from within a task? For the life of me I can't see a way to do it without global variable or singleton. No doubt I am about to learn something useful. :) – Gordon May 26 '20 at 12:41

2 Answers2

2

To complement Mathias R. Jessen's helpful answer - which may well be the best solution to your problem - with an answer to your original question:

So, what IS the right way (in PS 5.1) to create a single data structure that needs to be referenced by a lot of classes, and modified by some of them [without concern for thread safety]?

  • The main reason that global variables are to be avoided is that they are session-global, meaning that code that executes after your own sees those variables too, which can have side effects.

  • You cannot implement a true singleton in PowerShell, because PowerShell classes do not support access modifiers; notably, you cannot make a constructor private (non-public), you can only "hide" it with the hidden keyword, which merely makes it less discoverable while still being accessible.

  • You can approximate a singleton with the following technique, which itself emulates a static class (which PowerShell also doesn't support, because the static keyword is only supported on class members, not the class as a whole).

A simple example:

# NOT thread-safe
class AlmostAStaticClass {
  hidden AlmostAStaticClass() { Throw "Instantiation not supported; use only static members." }
  static [string] $Message    # static property
  static [string] DoSomething() { return ([AlmostAStaticClass]::Message + '!') }
}

[AlmostAStaticClass]::<member> (e.g., [AlmostAStaticClass]::Message = 'hi') can now be used in the scope in which AlmostAStaticClass was defined and all descendant scopes (but it is not available globally, unless the defining scope happens to be the global one).

If you need access to the class across module boundaries, you can pass it as a parameter (as a type literal); note that you still need :: to access the (invariably static) members; e.g.,
& { param($staticClass) $staticClass::DoSomething() } ([AlmostAStaticClass])


Implementing a thread-safe quasi-singleton - perhaps for use with ForEach-Object -Parallel (v7+) or Start-ThreadJob (v6+, but installable on v5.1) - requires more work:

Note:

  • Methods are then required to get and set what are conceptually properties, because PowerShell doesn't support code-backed property getters and setters as of 7.0 (adding this ability is the subject of this GitHub feature request).

  • You still need an underlying property however, because PowerShell doesn't support fields; again the best you can do is to hide this property, but it is technically still accessible.

The following example uses System.Threading.Monitor (which C#'s lock statement is based on) to manage thread-safe access to a value; for managing concurrent adding and removing items from collections, use the thread-safe collection types from the System.Collections.Concurrent namespace.

# Thread-safe
class AlmostAStaticClass {

  static hidden [string] $_message = ''  # conceptually, a *field*
  static hidden [object] $_syncObj = [object]::new() # sync object for [Threading.Monitor]

  hidden AlmostAStaticClass() { Throw "Instantiation not supported; use only static members." }

  static SetMessage([string] $text) {
    Write-Verbose -vb $text
    # Guard against concurrent access by multiple threads.
    [Threading.Monitor]::Enter([AlmostAStaticClass]::_syncObj)
    [AlmostAStaticClass]::_message = $text
    [Threading.Monitor]::Exit([AlmostAStaticClass]::_syncObj)
  }

  static [string] GetMessage() {
    # Guard against concurrent access by multiple threads.
    # NOTE: This only works with [string] values and instances of *value types*
    #       or returning an *element from a collection* that is 
    #       only subject to concurrency in terms of *adding and removing*
    #       elements.
    #       For all other (reference) types - entire (non-concurrent) 
    #       collections or individual objects whose properties are
    #       themselves subject to concurrent access, the *calling* code 
    #       must perform the locking.
    [Threading.Monitor]::Enter([AlmostAStaticClass]::_syncObj)
    $msg = [AlmostAStaticClass]::_message
    [Threading.Monitor]::Exit([AlmostAStaticClass]::_syncObj)
    return $msg
  }

  static [string] DoSomething() { return ([AlmostAStaticClass]::GetMessage() + '!') }
  
}

Note that, similar to crossing module boundaries, using threads too requires passing the class as a type object to other threads, which, however is more conveniently done with the $using: scope specifier; a simple (contrived) example:

# !! BROKEN AS OF v7.0
$class = [AlmostAStaticClass]
1..10 | ForEach-Object -Parallel { ($using:class)::SetMessage($_) }

Note: This cross-thread use is actually broken as of v7.0, due to classes currently being tied to the defining runspace - see this GitHub issue. It is to be seen if a solution will be provided.


As you can see, the limitations of PowerShell classes make implementing such scenarios cumbersome; using Add-Type with ad hoc-compiled C# code is worth considering as an alternative.

This GitHub meta issue is a compilation of various issues relating to PowerShell classes; while they may eventually get resolved, it is unlikely that PowerShell's classes will ever reach feature parity with C#; after all, OOP is not the focus of PowerShell's scripting language (except with respect to using preexisting objects).

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I am legit anxiety-sweating at the prospect of ever encountering a code base using this pattern ^_^ – Mathias R. Jessen May 26 '20 at 13:59
  • 1
    @MathiasR.Jessen My primary aim was to shed light on whether and how you can implement a singleton pattern in PowerShell (and in the process shed some light on PowerShell's limitations with respect to OOP / classes). Undoubtedly, it is generally preferable to avoid global data structures. I've added an example of a thread-safe quasi-singleton, but it's clear that both defining such a class and using it in PowerShell are cumbersome. – mklement0 May 26 '20 at 21:52
  • @MathiasR.Jessen: Turns out that using custom classes across threads with `ForEach-Object` is actually broken as of 7.0 - see https://github.com/PowerShell/PowerShell/issues/12801 – mklement0 May 27 '20 at 03:06
  • OK, this Dependency Injection is starting to make more sense. [See(https://gist.github.com/PragmaticPraxis/cb807c204dbde9e8512c78d373d80326) Now to try and grok mklement0's multithreaded additions. Not something I need now, but good to understand. – Gordon May 27 '20 at 06:33
  • 1
    @mklement0 yeah, I ran into similar recently with a concurrent collection wrapper - more than 4 iterations over `ForEach-Object` and I start losing data – Mathias R. Jessen May 27 '20 at 12:36
1

As mentioned in the comments, nothing in the code you linked to requires a singleton.

If you want to retain a parent-child relationship between your ProcessQueue and related Task instance, that can be solved structurally.

Simply require injection of a ProcessQueue instance in the Task constructor:

class ProcessQueue
{
  hidden [System.Collections.Generic.List[object]]$Queue = [System.Collections.Generic.List[object]]::New()
}

class Task
{
  [ProcessQueue]$Parent
  [string]$Id
  Task([string]$id, [ProcessQueue]$parent)
  {
    $this.Parent = $parent
    $this.Id = $id
  }
}

When instantiating the object hierarchy:

$myQueue = [ProcessQueue]::new()
$myQueue.Add([Task]@{ Id = "id"; Parent = $myQueue})

... or refactor ProcessQueue.Add() to take care of constructing the task:

class ProcessQueue
{
  [Task] Add([string]$Id){
    $newTask = [Task]::new($Id,$this)
    $Queue.Add($newTask)
    return $newTask
  }
}

At which point you just use ProcessQueue.Add() as a proxy for the [Task] constructor:

$newTask = $myQueue.Add($id)
$newTask.DisplayName = "Display name goes here"

Next time you need to search related tasks from a single Task instance, you just do:

$relatedTasks = $task.Parent.Find($whatever)
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • @mathias-r-jessen I will give this a try today. The thing I am not yet understanding is, if I am passing the entire data structure to the task, is that by reference? Since at the task level I need to resolve it, which means updating the task, which is contained in the ProcessQueue. If it works by reference that's grand, because then I could pass in either a parent or a root as needed. So, I guess the question becomes can I force the ProcessQueue to be passed by reference? I am going to give [Ref] a try with fingers crossed. – Gordon May 26 '20 at 12:48
  • 1
    You don't need `[ref]` - classes are reference types :) – Mathias R. Jessen May 26 '20 at 12:49
  • @mathias-r-jessen, this does beg one last question. Singleton is a very well documented pattern. So I wonder, IS there a time when a Singleton is the right answer in PowerShell? Or is Singleton a pattern that has fallen out of favor? Or PowerShell just isn't a good fit and there are better ways? And since I will need to pass that reference from `[ProcessQueue]` into all the child `[Set]`s, and those have child `[Package]`s, and all those have child `[Task]`s, and that's where I finally use the passed info. Singleton SEEMS so much easier, which is why I went down this road. – Gordon May 26 '20 at 12:57
  • Singleton's are only useful for providing exclusive resource access to multiple threads within the same process/appdomain. Since you don't require multi-threading of any kind, there's no point in implementing a singleton. For your nested hierarchy, you can repeat the exercise for all classes contained in another class in the hierarchy: inject an expected parent type. That being said, if your `[Task]`'s need to "see" eachother, that's in itself a smell that this object hierarchy you're designing is maybe not really fit for purpose? – Mathias R. Jessen May 26 '20 at 13:16