Archives 08/27/2023

An alternate (better) way – Strategy, Abstract Factory, and Singleton Design Patterns

Previously, we used the “long way” of implementing the Singleton pattern and had to implement a thread-safe mechanism. Now that classic is behind us. We can shorten that code and even get rid of the Create() method like this:

public class MySimpleSingleton
{
    public static MySimpleSingleton Instance { get; } = new MySimpleSingleton();
    private MySimpleSingleton() { }
}

The preceding code relies on the static initializer to ensure that only one instance of the MySimpleSingleton class is created and assigned to the Instance property.

This simple technique should do the trick unless the singleton’s constructor executes some heavy processing.

With the property instead of a method, we can use the singleton class like this:

MySimpleSingleton.Instance.SomeOperation();

We can prove the correctness of that claim by executing the following test method:

[Fact]
public void Create_should_always_return_the_same_instance()
{
    var first = MySimpleSingleton.Instance;
    var second = MySimpleSingleton.Instance;
    Assert.Same(first, second);
}

It is usually best to delegate responsibilities to the language or the framework whenever possible like we did here with the property initializer. Using a static constructor would also be a valid, thread-safe alternative, once again delegating the job to language features.

Beware of the arrow operator.

It may be tempting to use the arrow operator => to initialize the Instance property like this: public static MySimpleSingleton Instance => new MySimpleSingleton();, but doing so would return a new instance every time. This would defeat the purpose of what we want to achieve. On the other hand, the property initializer runs only once.

The arrow operator makes the Instance property an expression-bodied member, equivalent to creating the following getter: get { return new MySimpleSingleton(); }. You can consult Appendix A for more information about expression-bodies statements.

Before we conclude the chapter, the Singleton (anti-)pattern also leads to a code smell.

Code smell – Ambient Context

That last implementation of the Singleton pattern led us to the Ambient Context pattern. We could even call the Ambient Context an anti-pattern, but let’s just state that it is a consequential code smell.I do not recommend using ambient contexts for multiple reasons. First, I do my best to avoid anything global; an ambient context is a global state. Globals, like static members in C#, can look very convenient because they are easy to access and use. They are always there and accessible whenever needed: easy. However, they bring many drawbacks in terms of flexibility and testability.When using an ambient context, the following occurs:

  • Tight coupling: global states lead to less flexible systems; consumers are tightly coupled with the ambient context.
  • Testing difficulty: global objects are harder to replace, and we cannot easily swap them for other objects, like a mock.
  • Unforseen impacts: if some part of your system messes up your global state, that may have unexpected consequences on other parts of your system, and you may have difficulty finding out the root cause of those errors.
  • Potential misuse: developers could be tempted to add non-global concerns to the ambient context, leading to a bloated component.