如何在允许 configurability/setup 的情况下重构有状态单例?
How to refactor stateful singleton while still allowing configurability/setup?
我希望按照 here. The change is to Agent
in Elastic APM Agent which can be seen in GitHub here 所述的 System.Lazy<T>
设计对标准单例设计模式进行更改。这是为简洁起见分解的代码:
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;
}
这个明显的问题是,如果在调用Agent.Setup
之前访问Agent.Instance
,Agent.Lazy
中的Foo
对象被实例化为null(_bar
) 传递给它的构造函数。因此,传递给 Setup
的 Bar
对象将用于基础 Foo
的预期将不会得到满足。
当然,问题在于这是一个反模式,如 here 所述,因为这个单例封装了全局状态。正如这个 link 描述的那样:
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.
article提倡使用DI来解决这个问题。但是,是否有更简单的方法来解决这种无法进行 DI 或涉及过多重构的情况?
好吧,作为一种选择,您可以使用类似的东西
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));
}
}
我希望按照 here. The change is to Agent
in Elastic APM Agent which can be seen in GitHub here 所述的 System.Lazy<T>
设计对标准单例设计模式进行更改。这是为简洁起见分解的代码:
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;
}
这个明显的问题是,如果在调用Agent.Setup
之前访问Agent.Instance
,Agent.Lazy
中的Foo
对象被实例化为null(_bar
) 传递给它的构造函数。因此,传递给 Setup
的 Bar
对象将用于基础 Foo
的预期将不会得到满足。
当然,问题在于这是一个反模式,如 here 所述,因为这个单例封装了全局状态。正如这个 link 描述的那样:
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.
article提倡使用DI来解决这个问题。但是,是否有更简单的方法来解决这种无法进行 DI 或涉及过多重构的情况?
好吧,作为一种选择,您可以使用类似的东西
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));
}
}