I wish to make a change to a standard singleton design pattern that follows the System.Lazy<T>
design as described here. The change is to Agent
in Elastic APM Agent which can be seen in GitHub here. This is the code broken down for brevity:
public static class Agent
{
private static readonly Lazy<Foo> Lazy = new Lazy<Foo>(() => new Foo(_bar));
private static Bar _bar;
public static Foo Instance => Lazy.Value;
public static bool IsInstanceCreated => Lazy.IsValueCreated;
public static void Setup(Bar bar) => _bar = bar;
}
The clear problem with this is that if Agent.Instance
is accessed before Agent.Setup
is called, the Foo
object in Agent.Lazy
is instantiated with null (_bar
) passed to its constructor. Therefore, the expectation that the Bar
object passed to Setup
will be used for the underlying Foo
will not be met.
The problem of course is that this is an antipattern as described here, because this singleton is encapsulating global state. As this link describes:
A singleton is a convenient way for accessing the service from anywhere in the application code.
The model quickly falls apart when the service not only provides access to operations but also encapsulates state, which affects how other code behaves. Application configuration is a good example of this. In the best case, the configuration is read once at the application start and does not change for the entire lifetime of the application.
However, different configuration can cause a method to return different results although no visible dependencies have changed, i.e. the constructor and the method have been called with the same parameters. This can become an even bigger problem if the singleton state can change at runtime, either by rereading the configuration file or by programmatic manipulation. Such code can quickly become very difficult to reason with:
var before = new MyClass().CalculateResult(3, 2);// depends on Configuration.Instance RefreshConfiguration(); // modifies values in Configuration.Instance var after = new MyClass().CalculateResult(3, 2); // depends on Configuration.Instance
Without comments, an uninformed reader of the code above could not expect the values of
before
andafter
to be different, and could only explain it after looking into the implementation of the individual methods, which read and modify global state hidden inConfiguration
singleton.
The article advocates using DI to resolve this. However, is there a simpler way to resolve this situation where DI is not possible or would involve too much of a refactor?