Archives September 2023

Fun fact – Strategy, Abstract Factory, and Singleton Design Patterns

Many years ago, before the JavaScript frameworks era, I fixed a bug in a system where some function was overriding the value of undefined due to a subtle error. This is an excellent example of how global variables could impact your whole system and make it more brittle. The same applies to the Ambient Context and Singleton patterns in C#; globals can be dangerous and annoying.

Rest assured that, nowadays, browsers won’t let developers update the value of undefined, but it was possible back then.

Now that we’ve discussed global objects, an ambient context is a global instance, usually available through a static property. The Ambient Context pattern can bring good things, but it is a code smell that smells bad.

There are a few examples in .NET Framework, such as System.Threading.Thread.CurrentPrincipal and System.Threading.Thread.CurrentThread, that are scoped to a thread instead of being purely global like most static members. An ambient context does not have to be a singleton, but that is what they are most of the time. Creating a non-global (scoped) ambient context is harder and requires more work.

Is the Ambient Context pattern good or bad? I’d go with both! It is useful primarily because of its convenience and ease of use. Most of the time, it could and should be designed differently to reduce its drawbacks.There are many ways of implementing an ambient context, but to keep it brief and straightforward, we are focusing only on the singleton version of the ambient context. The following code is a good example:

public class MyAmbientContext
{
    public static MyAmbientContext Current { get; } = new MyAmbientContext();
    private MyAmbientContext() { }
    public void WriteSomething(string something)
    {
        Console.WriteLine($”This is your something: {something}”);
    }
}

That code is an exact copy of the MySimpleSingleton class, with a few subtle changes:

  • Instance is named Current.
  • The WriteSomething method is new but has nothing to do with the Ambient Context pattern itself; it is just to make the class do something.

If we take a look at the test method that follows, we can see that we use the ambient context by calling MyAmbientContext.Current, just like we did with the last singleton implementation:

[Fact]
public void Should_echo_the_inputted_text_to_the_console()
{
    // Arrange (make the console write to a StringBuilder
    // instead of the actual console)
    var expectedText = “This is your something: Hello World!”
+ Environment.NewLine;
    var sb = new StringBuilder();
    using (var writer = new StringWriter(sb))
    {
        Console.SetOut(writer);
        // Act
        MyAmbientContext.Current.WriteSomething(“Hello World!”);
    }
    // Assert
    var actualText = sb.ToString();
    Assert.Equal(expectedText, actualText);
}

The property could include a public setter or support more complex logic. Building the right classes and exposing the right behaviors is up to you and your specifications.To conclude this interlude, avoid ambient contexts and use instantiable classes instead. We see how to replace a singleton with a single instance of a class using dependency injection in the next chapter. That gives us a more flexible alternative to the Singleton pattern. We can also create a single instance per HTTP request, which saves us the trouble of coding it while eliminating the disadvantages.