Не нужно путать паттерн Singleton с жизненными циклами singleton и scoped-singleton сервиса в сервис-контейнере. Несмотря на общее название, это разные вещи.
Является ли шаблон проектирования Singleton антипаттерном?
Любой паттерн в неопытных руках может превратиться в антипаттерн, добавить больше энтропии и привести проект в большой ком грязи, который будет очень трудно распутать. Многие привыкли думать, что, поддерживать проект в хорошем состоянии нужно только разработчику, чтобы удовлетворять внутреннего перфекциониста, а значит лучше сконцентрироваться на скорости разработки, а не на качестве кодовой базы и долгосрочной поддержке, особенно в условиях эволюции бизнеса.
В реальности же любой код, написанный разработчиком, является обязательством по отношению к организации, для которой этот код написан. К команде, которая в этом же коде работает, к конечным пользователям системы. И уже в последнюю очередь сам разработчик обязан перед собой самосовершенствоваться и следить за своим профессиональным ростом.
Книга Design Patterns: Elements of Reusable Object-Oriented Software авторства Gang of Four переиздавалась множество раз, и каждый раз в разделе порождающих паттернов оказывался Singleton, хотя он, вроде как, считается антипаттерном и в приличном обществе презирается.
Если это антипаттерн, почему от него не откажутся? Почему о нем не забудут как о страшном сне? Попахивает каким-то заговором.
Всю аргументацию противников этого паттерна можно свести к одному: паттерн расширяет глобальное состояние программы и усложняет тестирование, вплоть до его невозможности. И вообще лучше использовать инверсию контроля (IoC), которую этот паттерн нарушает.
И в чем же они не правы? Давайте порассуждаем.
Глобальное состояние
Верно, назначение паттерна заключается в обеспечении одного экземпляра класса и предоставлением к нему глобальной точки доступа.
Он расширяет глобальное состояние программы, предоставляет как уточнение имен, так и инкапсулирует (хотя это совсем не обязательно) контроль за количеством экземпляров. Да, Singleton, несмотря на свое название и назначение, позволяет и способствует тому, что экземпляров может быть несколько, но не будем заострять на этом внимание.
Чем же плохо глобальное состояние программы? Тем, что глобальное состояние, как это ни очевидно, глобально, то есть доступно из любого места в коде.
Глобальное состояние затрудняет контроль зависимостей, так как, в отличие от внедрения зависимостей, где зависимости появляются явным образом, глобальное состояние не требует явности (и, как следствие, ясности). Чтобы узнать, что код использует глобальное состояние, необходимо углубиться в детали реализации в том числе и при тестировании.
Проблемы глобального состояния — это общие проблемы, они связаны с паттерном Singleton лишь косвенно. Их появляние зависит от того, как именно и где этот паттерн будет использоваться, и от того, насколько это оправданно.
В описании этого паттерна нет ни слова о том, что Singleton стоит использовать вообще везде. Это додумали сами разработчики.
Тестирование
Становится ли невозможным написание тестов, когда мы говорим о модульном тестировании? Вовсе нет. Паттерн совершенно не запрещает подменять порождаемый экземпляр и регистрировать его во внутреннем реестре.
Другое дело, что в модульном тесте, где тестируется конкретная единица поведения, мы должны тестировать поведение, без необходимости раскрывать детали имплементации. Знание о том, что под капотом у функции используется неявная зависимость — это раскрытие тех самых деталей, что заставляет нас смещать фокус с тестирования черного ящика и его результата на подмену тестируемого поведения и соблюдения необходимых условий.
Само собой, это увеличивает хрупкость теста.
Контроль экземпляров
Хочу обратить внимание на то, что паттерн относится к группе порождающих паттернов.
Простыми словами, он нужен в первую очередь для порождения конкретного экземпляра (или предсказуемого набора экземпляров), чтобы не допустить создания лишних. А уже во вторую очередь идет предоставление глобальной точки доступа из-за помещения этого экземпляра в глобальное состояние.
Инверсия контроля
Нарушает ли паттерн принцип инверсии контроля (IoC)? Здесь нам нужно быть осторожными с выводами.
В первую очередь хочется сказать, что IoC — это не про интерфейсы. Кроме того, бесконтрольное использование интерфейсов может привести к деструктивной развязке (destructive decoupling), при которой мы получаем одновременно слабое связывание (low coupling) и слабое сцепление (low cohesion), поэтому с ними нужно быть аккуратным.
Этот принцип предполагает, что сами объекты или их зависимости не определяют, как и когда они используются. Его основная идея заключается в том, что код не должен управлять своими зависимостями, вместо этого контроль зависимостей делегируется внешнему управлению.
Поэтому в этом плане можно считать, что Singleton нарушает этот принцип, поскольку сам контролирует создание этого экземпляра, несмотря на то, что оно инкапсулировано.
Когда уместно?
Хорошим примером может служить отлов ошибок приложения при использовании Sentry.
Есть некий глобальный обработчик ошибок, который требуется проинициализировать перед запуском приложения, а после запуска внедрить тот же самый экземпляр обработчика в сервис-контейнер для того, чтобы использовать его при необходимости уже как человеческую зависимость.
Без этого паттерна нам пришлось бы или создавать экземпляр и каким-либо образом прокидывать в уже созданное приложение, или инициализировать экземпляр в рамках жизненного цикла приложения. Первый вариант предполагает, что сам используемый фреймворк должен иметь такую возможность. Второй вариант подразумевают, что приложение должно уже быть в каком-то рабочем состоянии, чтобы выполнение кода дошло до регистрации обработчика ошибок, что не позволяет нам перехватывать ошибки инициализации.
Вторым хорошим примером может быть ситуация, когда мы никак не можем повлиять на жизненый цикл приложения. Например при использовании игрового движка или в мобильной разработке. В обеих ситуациях за контроль создания компонентов приложения отвечает платформа, где нам недоступна возможность явного внедрения зависимостей через конструкторы и создание компонентов приложения через сервис-контейнер.
Можно получить некий глобальный сервис-локатор, взять из него все нужные зависимости и таким образом хотя бы частично реализовать явность зависимостей там, где иначе просто не сделать. Важно понимать, что этот паттерн хорошо работает в связке с другими, он помогает породить объект и использовать именно его там, где возможность получить его через IoC в принципе отсутствует.
Ему следует предпочесть использовать IoC, так как этот принцип имеет существенно больше плюсов, выигрыш будет более очевиден.
В заключении
Если паттерн в неумелых руках настолько опасен, так почему же он постоянно присутствует от издания к изданию в книге авторства GoF? Потому что это паттерн в вакууме. Это некоторый подход, инструмент, который может в некоторых ситуациях облегчить жизнь, особенно, если иного сделать в принципе невозможно.
Выбор и применение паттернов зависисит всегда и исключительно от разработчика. Паттерн Singleton не виноват в том, что разработчик может не знать проблем глобального состояния и стремиться везде это глобальное состояние ввести, вне зависимости от целесообразности.
Является ли шаблон проектирования Singleton антипаттерном? Нет. Есть антилюди, которые делают его таковым.