Как провести рефакторинг синглтона с отслеживанием состояния, сохранив при этом возможность настройки / настройки?
Я хочу внести изменения в стандартный шаблон проектирования 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));
}
}