1

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 and after to be different, and could only explain it after looking into the implementation of the individual methods, which read and modify global state hidden in Configuration 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?

Community
  • 1
  • 1
Neo
  • 4,145
  • 6
  • 53
  • 76
  • 1
    In your particular case you can check `IsInstanceCreated` property before accessing an `Instance`. Or init the lazy value inside setup method – Pavel Anikhouski Nov 18 '19 at 14:52

1 Answers1

1

Well, as an option, you can use something like that

public static class Agent
{
    private static Lazy<Foo> _lazy;

    public static Foo Instance => _lazy?.Value ?? throw new InvalidOperationException("Please, setup the instance");
    public static bool IsInstanceCreated => _lazy?.IsValueCreated ?? false;

    public static void Setup(Bar bar)
    {
        _lazy = new Lazy<Foo>(() => new Foo(bar));
    }
}
Pavel Anikhouski
  • 21,776
  • 12
  • 51
  • 66
  • This is a good solution. It would certainly be a breaking change as it means users of the API who are calling `Agent.Instance` before `Agent.Setup` already (knowing that it will use the default configuration for `Foo`, i.e., `null` passed into its constructor) will suddenly see an exception. But I'll see what the Elastic APM Agent authors think of it [here](https://github.com/elastic/apm-agent-dotnet/issues/596#issuecomment-555043864) before moving forward with this solution. – Neo Nov 18 '19 at 15:10
  • 1
    You can return `null` value instead of exception, it depends on your needs and requirements, as usual:) Checking `IsInstanceCreated` will help to prevent some errors and side effects – Pavel Anikhouski Nov 18 '19 at 15:13
  • True, but it would still be a breaking change as their code may not check for null because it wasn't expected before. – Neo Nov 18 '19 at 15:15
  • 1
    On the other hand, they will get a `Foo` instance which isn't properly initialized and probably get an exception later. Imho, it's better to let the consumer know what is an error right now rather than get an unclear exception with `Foo` instance later – Pavel Anikhouski Nov 18 '19 at 15:19
  • I agree. Definitely up for throwing an exception (such as `InvalidOperationException`) as early as possible when the system has been incorrectly used. Implementing it right now. :) – Neo Nov 18 '19 at 15:20
  • Please, don't forget to approve the answer, if it'll solve your problem – Pavel Anikhouski Nov 18 '19 at 15:29
  • Of course. I will do so once I get the opinion of those I mentioned, and failing any other better answers, I'll then accept yours. In terms of the implementation I'm doing, your solution broke too many unit tests and I know would not be accepted as it's a breaking change (that doesn't mean it's not a solution generally of course). So in this case, I'm just going to add a public property `public static bool IsConfigured { get; private set; }` and set it to `true` at the end of `Setup`. Users can then make use of this property while not breaking previous uses of the code. – Neo Nov 18 '19 at 16:00
  • Well, `IsConfigured` in this case means almost the with `IsInstanceCreated` in my snippet, except the fact that consumer gets the `Foo` instance with null `Bar` reference instead of `null` – Pavel Anikhouski Nov 18 '19 at 16:13
  • Almost, exactly. `IsConfigured` would return true if `_lazy` is not null even if `_lazy.IsValueCreated` returns false, while `IsInstanceCreated` must only return true if `_lazy.IsValueCreated` returns true. `IsConfigured` is to ensure that `Setup` has been called while `IsInstanceCreated` is to check if the `Lazy` object has already been evaluated. – Neo Nov 18 '19 at 16:41