1

我希望对遵循此处System.Lazy<T>描述的设计的标准单例设计模式进行更改。更改是在 Elastic APM 代理中,可以在 GitHub 中看到。为简洁起见,这是分解的代码:Agent

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.Instance在调用之前访问过Agent.Setup,则Foo对象 in被实例化为传递给其构造函数Agent.Lazy的 null ( )。因此,传递给的对象将用于底层证券_bar的期望将不会得到满足。BarSetupFoo

问题当然是这是一个反模式,因为这个单例封装全局状态。正如此链接所述:

单例是从应用程序代码中的任何位置访问服务的便捷方式。

当服务不仅提供对操作的访问而且还封装状态,这会影响其他代码的行为方式时,该模型很快就会崩溃。应用程序配置就是一个很好的例子。在最好的情况下,配置在应用程序启动时被读取一次,并且在应用程序的整个生命周期内都不会改变。

然而,不同的配置可能导致方法返回不同的结果,尽管没有可见的依赖关系发生变化,即构造函数和方法已使用相同的参数调用。如果单例状态可以通过重新读取配置文件或通过编程操作在运行时更改,这可能会成为一个更大的问题。这样的代码很快就会变得非常难以推理:

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

如果没有注释,不了解上述代码的读者无法期望 和 的值before不同after,只能在查看各个方法的实现后进行解释,这些方法读取和修改隐藏在Configuration单例中的全局状态。

文章提倡使用 DI 来解决这个问题。但是,有没有更简单的方法来解决这种情况,即 DI 是不可能的或涉及太多重构?

4

1 回答 1

1

好吧,作为一种选择,您可以使用类似的东西

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));
    }
}
于 2019-11-18T14:59:13.153 回答