Search code examples
powershellsingletonglobal-variables

Alternative to Global Variable or Singleton


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. :)


Solution

  • 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)