Почему Singleton считают антипаттерном?

Ответ

Singleton часто критикуют как антипаттерн из-за ряда архитектурных проблем, с которыми я сталкивался на практике:

  1. Глобальное состояние и скрытые зависимости: Класс, доступный глобально через getInstance(), нарушает принципы инкапсуляции и внедрения зависимостей (DI). Это усложняет понимание того, от каких компонентов зависит ваш код, что особенно проблематично в больших приложениях.
  2. Сложность тестирования: Из-за глобального состояния становится практически невозможно изолировать модульные тесты. Состояние, измененное в одном тесте, может «протекать» и влиять на результаты других.
  3. Нарушение принципа единственной ответственности (SRP): Singleton часто становится «мусорным ведром» для функциональности, которая просто должна быть доступна в одном экземпляре, но логически не связана.
  4. Проблемы в многопоточных средах: Требует дополнительной синхронизации (двойная проверка, мьютексы), что усложняет реализацию и может стать узким местом по производительности.

Пример проблемного Singleton на C#:

public sealed class ProblematicLogger
{
    private static ProblematicLogger _instance;
    private static readonly object _lock = new object();

    private ProblematicLogger() { }

    public static ProblematicLogger Instance
    {
        get
        {
            if (_instance == null) // Первая проверка (непотокобезопасная)
            {
                lock (_lock)
                {
                    if (_instance == null) // Вторая проверка
                    {
                        _instance = new ProblematicLogger();
                    }
                }
            }
            return _instance;
        }
    }
    // ... методы логирования
}

Альтернативы, которые я предпочитаю:

  • Внедрение зависимостей: Зарегистрировать сервис как singleton в контейнере DI (например, в ASP.NET Core services.AddSingleton<ILogger, FileLogger>()). Это дает тот же жизненный цикл объекта, но без глобального доступа и со всеми преимуществами DI.
  • Явная передача экземпляра: Создать экземпляр в корне приложения (в Program.cs или Startup.cs) и явно передавать его в зависимости через конструктор.

Singleton может быть оправдан для по-настоящему уникальных, неделимых ресурсов (например, доступ к физическому устройству), но в 95% случаев его стоит избегать в пользу DI.

Ответ 18+ 🔞

А, блядь, Singleton! Ну это же классика, ёпта, прямо как дедушка всех антипаттернов, который всех уже заебал, но всё равно лезет в каждый второй проект, как назойливая муха.

Смотри, в чём прикол-то. Вроде бы удобно — один экземпляр на всё приложение, и все к нему ломятся через getInstance(). Красота? А вот хуй там! Это же глобальное состояние, самый натуральный распиздяй в архитектуре. Ты смотришь на класс и нихуя не понимаешь, кто от него зависит. Зависимости эти, блядь, скрытые, как хитрая жопа — вроде нет её, а на самом деле везде засветилась. Нарушает все принципы нормального дизайна, особенно инкапсуляцию. Вместо чистого внедрения через конструктор — получаешь эту манду с ушами, которая торчит отовсюду.

А про тестирование я вообще молчу. Сложность тестирования — это пиздец просто. Один тест пописал в глобальное состояние, а следующий уже в этой луже плавает и охуевает, почему у него всё падает. Изоляция? Да похуй! Терпения ноль ебать, когда пытаешься написать нормальные юнит-тесты, а этот синглтон всё портит.

И ещё он часто нарушает принцип единственной ответственности. Превращается в такое мусорное ведро, куда скидывают всё подряд, лишь бы было в одном экземпляре. Логика размазывается, класс раздувается — красота, ебать мои старые костыли.

Ну и вишенка на торте — проблемы в многопоточных средах. Чтобы сделать его хоть как-то безопасным, нужно городить огород из проверок и мьютексов. Смотри, какой ужас обычно пишут на C#:

public sealed class ProblematicLogger
{
    private static ProblematicLogger _instance;
    private static readonly object _lock = new object();

    private ProblematicLogger() { }

    public static ProblematicLogger Instance
    {
        get
        {
            if (_instance == null) // Первая проверка (непотокобезопасная)
            {
                lock (_lock)
                {
                    if (_instance == null) // Вторая проверка
                    {
                        _instance = new ProblematicLogger();
                    }
                }
            }
            return _instance;
        }
    }
    // ... методы логирования
}

Ёперный театр! Двойная проверка, замок... И всё равно есть шанс накосячить. И это становится узким местом, все потоки в очередь выстраиваются, как за халявой.

Так что же делать, спросишь? А альтернативы-то есть, и они овердохуища лучше.

Во-первых, внедрение зависимостей (DI). Зарегистрировал сервис как singleton в контейнере (в том же ASP.NET Core — services.AddSingleton<ILogger, FileLogger>()) и всё. Жизненный цикл тот же — один экземпляр на всё приложение. Но глобального доступа нет! Зависимости явные, тестируется легко, архитектура чистая. Небо и земля.

Во-вторых, явная передача экземпляра. Создал объект в корне приложения (в Program.cs) и передал туда, куда нужно, через конструктор. Всё прозрачно, как слёзы младенца.

Singleton, конечно, не совсем умер. Для по-настоящему уникальных штук, вроде доступа к конкретному физическому железу, он ещё может сгодиться. Но в 95% случаев, чувак, это путь в ад. Лучше обойти его стороной, как лужицу с неясным содержимым.