Как провести рефакторинг синглтона с отслеживанием состояния, сохранив при этом возможность настройки / настройки?

Я хочу внести изменения в стандартный шаблон проектирования singleton, который следует System.Lazy<T>дизайн, как описано здесь. Изменение заключается вAgentв Elastic APM Agent, который можно увидеть на GitHub здесь. Это код для краткости:

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 объект в Agent.Lazy создается с помощью null (_bar) передается его конструктору. Следовательно, ожидание того, чтоBar объект передан Setup будет использоваться для базового Foo встречаться не будет.

Проблема, конечно, в том, что это описанный здесь антипаттерн, потому что этот синглтон инкапсулирует глобальное состояние. Как описывает эта ссылка:

Синглтон - это удобный способ доступа к сервису из любой точки кода приложения.

Модель быстро разваливается, когда служба не только предоставляет доступ к операциям, но и инкапсулирует состояние, которое влияет на поведение другого кода. Конфигурация приложения - хороший тому пример. В лучшем случае конфигурация считывается один раз при запуске приложения и не меняется в течение всего времени существования приложения.

Однако разная конфигурация может привести к тому, что метод будет возвращать разные результаты, хотя видимые зависимости не изменились, т.е. конструктор и метод были вызваны с одинаковыми параметрами. Это может стать еще большей проблемой, если одноэлементное состояние может измениться во время выполнения, либо путем перечитывания файла конфигурации, либо с помощью программных манипуляций. Такой код может быстро стать очень трудным для понимания:

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 невозможен или требует слишком большого рефакторинга?

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));
    }
}
Другие вопросы по тегам