简体   繁体   中英

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

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> (eg, [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; eg,
& { 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).

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM