Почему синглтон антипаттерн
Перейти к содержимому

Почему синглтон антипаттерн

  • автор:

Синглтоны и общие экземпляры

Каждый раз при обсуждении программного обеспечения с другими разработчиками всплывает тема синглтонов, особенно в контексте развития WordPress’а. Я часто пытаюсь объяснить, почему их надо избегать, даже если они считаются стандартным шаблоном.

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

Что такое синглтон?

Синглтон — это шаблон проектирования в разработке программного обеспечения, описанный в книге Design Patterns: Elements of Reusable Object-Oriented Software (авторы — Банда четырёх), благодаря которой о шаблонах проектирования заговорили как об инструменте разработки ПО.

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

Это на самом деле достаточно просто объяснить и понять, и для многих людей синглтон — это лёгкий вход в мир шаблонов проектирования, что делает его самым популярным шаблоном.

Синглтон популярен, он был одним из первых шаблонов, описанных и стандартизированных в книге. Как же так получается, что некоторые разработчики считают его антишаблоном? Неужели он может быть настолько плохим?

Но синглтоны полезны и важны!

Я заметил, что многие люди путают два смежных понятия. Когда они говорят, что им нужен синглтон, им на самом деле нужно использовать один экземпляр объекта в разных операциях инстанцирования. В общем, когда вы создаёте инстанс, вы создаёте новый экземпляр этого класса. Но для некоторых объектов нужно всегда использовать один и тот же общий экземпляр (shared instance) объекта, вне зависимости от того, где он используется.

Но синглтон не является верным решением для этого.

Путаница вызвана тем, что синглтон объединяет две функции (responsibilities) в одном объекте. Допустим, есть синглтон для подключения к базе данных. Давайте назовём его (очень изобретательно) DatabaseConnection . У синглтона теперь две главных функции:

  1. Управление подключением.
  2. Управление инстансами DatabaseConnection .

Именно из-за второй функции люди выбирают синглтон, но эту задачу должен решать другой объект.

Нет ничего плохого в общем экземпляре. Но объект, который вы хотите для этого использовать, — не место для такого ограничения.

Ниже я покажу несколько альтернатив. Но сначала хочу рассказать, какие проблемы может вызвать синглтон.

Проблемы с синглтоном

Синглтон и SOLID

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

  • Sпринцип единственной ответственности. Очевидно, что синглтон противоречит ему, как уже говорилось раньше.
  • Oпринцип открытости/закрытости: объекты должны быть открыты для расширения, но закрыты для изменения. Синглтон нарушает данный принцип, так как контролирует точку доступа и возвращает только самого себя, а не расширение.
  • Lпринцип подстановки Барбары Лисков: объекты могут быть заменены экземплярами своих подтипов без изменения использующего их кода. Это неверно в случае с синглтоном, потому что наличие нескольких разных версий объекта означает, что это уже не синглтон.
  • Iпринцип разделения интерфейса: много специализированных интерфейсов лучше, чем один универсальный. Это единственный принцип, который синглтон нарушает не напрямую, но лишь потому, что он не позволяет использовать интерфейс.
  • Dпринцип инверсии зависимостей: вы должны зависеть только от абстракций, а не от чего-то конкретного. Синглтон нарушает его, потому что в данном случае зависеть можно только от конкретного экземпляра синглтона.

Шаблон синглтон нарушает четыре из пяти принципов SOLID. Он, возможно, хотел бы нарушить и пятый, если бы только мог иметь интерфейсы…

Легко сказать, что ваш код не работает только из-за каких-то теоретических принципов. И хотя, согласно моему собственному опыту, эти принципы — самое ценное и надёжное руководство, на которое можно опираться при разработке программного обеспечения, я понимаю, что просто слова «это факт» для многих звучат неубедительно. Мы должны проследить влияние синглтона на вашу повседневную практику.

Использования шаблона синглтон

Здесь перечислены недостатки, с которыми можно столкнуться, если иметь дело с синглтоном:

  • Вы не можете передавать/внедрять аргументы в конструктор. Поскольку при первом вызове синглтона реально исполняется только конструктор и вы не можете знать заранее, какой код первым обратится к синглтону, то во всём потребляющем коде нужно использовать один и тот же набор аргументов для передачи в конструктор, что во многих случаях почти невыполнимо, а вначале вообще бессмысленно. В результате синглтон делает бесполезным основной механизм инстанцирования в ООП-языках.
  • Вы не можете имитировать (mock away) синглтон при тестировании компонентов, использующих его. Это делает почти невозможным корректное модульное тестирование, потому что вы не добьётесь полной изоляции тестируемого кода. Проблема вызвана даже не самой логикой, которую вы хотите тестировать, а произвольным ограничением инстанцирования, в которое вы её оборачиваете.
  • Так как синглтон — глобально доступная конструкция, которая используется всей вашей кодовой базой, то идут прахом любые усилия по инкапсуляции, отчего появляются те же проблемы, что и в случае с глобальными переменными. То есть, как бы вы ни пытались изолировать синглтон в инкапсулированной части кода, любой другой внешний код может привести к побочным эффектам и багам в синглтоне. А без надлежащей инкапсуляции выхолащиваются сами принципы ООП.
  • Если у вас когда-либо был сайт или приложение, разросшееся настолько, что синглтону DatabaseConnection неожиданно понадобилось подключение ко второй, отличной от первой базе данных, значит, вы в беде. Придётся заново пересмотреть саму архитектуру и, возможно, полностью переписать значительную часть кода.
  • Все тесты, прямо или косвенно использующие синглтон, не могут корректно переключаться с одного на другой. Они всегда сохраняют состояние посредством синглтона, что может привести к неожиданному поведению там, где ваши тесты зависят от очерёдности запуска, или оставшееся состояние скроет от вас реальные баги.
  • Своё одиночное инстанцирование синглтоны принудительно применяют к пространству текущего процесса, что подходит для статичной области видимости. Это означает появление проблем с распараллеливанием, когда у вас несколько процессов или распределённое выполнение. Это должно намекать на вероятность того, что синглтоны — всего лишь ошибочная концепция, которая ломается сразу же, как только вы начинаете работать в распределённой системе.

Альтернативы синглтону

Я не хочу быть тем человеком, который во всём видит плохое, но не может предложить решение проблемы. Хотя я считаю, что вы должны оценивать всю архитектуру приложения, чтобы решить, как первым делом избежать использования синглтона, я предлагаю несколько самых распространённых способов в WordPress, когда синглтон можно легко заменить механизмом, который удовлетворяет всем требованиям и лишён большинства недостатков. Но прежде чем рассказать об этом, я хочу отметить, почему все мои предложения — только компромисс.

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

  1. Инстанцирование App (нужны Config , Database , Controller ).
  2. Инстанцирование Config для внедрения в App .
  3. Инстанцирование Database для внедрения в App .
  4. Инстанцирование Controller для внедрения в App (нужны Router , Views ).
  5. Инстанцирование Router для внедрения в Controller (нужен HTTPMiddleware ).

С помощью одного вызова выстроится сверху донизу весь стек приложения с внедрением зависимостей по мере необходимости. Цели такого подхода:

  • У каждого объекта есть точный список нужных ему зависимостей, и только их он должен использовать. Когда что-либо ломается, вы можете легко изолировать ответственный за это код.
  • Тесной связи между объектами нет, все объекты при использовании внедрённых реализаций полагаются только на интерфейсы.
  • Глобального статуса нет. Каждое отдельное поддерево, стоящее выше по иерархии, будет корректно изолировано от остальных, благодаря чему разработчик не наделает багов в модуле Б, изменяя модуль А.

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

Держите это в уме, пока мы будем обсуждать подходы. Идеальное решение, при котором весь стек WordPress’а инстанцируется через централизованный механизм внедрения, нам недоступно, поскольку оно требует поддержки WordPress Core. Всем описываемым далее подходам свойственны те или иные общие недостатки вроде сокрытия зависимостей посредством обращения к ним напрямую из логики вместо их внедрения.

Код синглтона

Пример кода с использованием синглтон-подхода, который мы будем сравнивать с другими:

// Синглтон. final class DatabaseConnection < private static $instance; private function __construct() <>// Вызов для получения одного настоящего экземпляра. public static function get_instance() < if ( ! isset( self::$instance ) ) < self::$instance = new self(); >return self::$instance; > // Код логики смешан с кодом механизма инстанцирования. public function query( . $args ) < // Исполняется запрос и возвращается результат. >> // Потребляющий код. $database = DatabaseConnection::get_instance(); $result = $database->query( $query );

Я не включил сюда все подробности реализации, с которыми часто загружаются синглтоны, потому что они неважны для теоретической дискуссии.

Фабричный метод

В большинстве случаев лучший способ уйти от проблем, связанных с синглтоном, — использовать шаблон проектирования «фабричный метод». Фабрика — это объект, чья единственная обязанность — инстанцировать другие объекты. Вместо DatabaseConnectionManager , который делает собственный экземпляр с помощью метода get_instance() , у вас есть DatabaseConnectionFactory , создающий экземпляры объекта DatabaseConnection . В общем, фабрика всегда будет производить новые экземпляры нужного объекта. Но на основании запрошенного объекта и контекста фабрика может сама решать, создавать ли новый экземпляр или всегда расшаривать какой-то один.

Учитывая название шаблона, вы можете подумать, что он больше похож на код Java, чем PHP-код, так что не стесняйтесь отклоняться от слишком строгого (и ленивого) соглашения об именовании и называйте фабрику более изобретательно.

Пример фабричного метода:

// Фабрика. final class Database < public function get_connection(): DatabaseConnection < static $connection = null; if ( null === $connection ) < // Здесь может быть произвольная логика, решающая, какую реализацию использовать. $connection = new MySQLDatabaseConnection(); >return $connection; > > // Здесь у нас интерфейс, так что вы можете работать с несколькими реализациями и корректно имитировать (mock) ради тестирования. interface DatabaseConnection < public function query( . $args ); >// Используемая в данный момент реализация. final class MySQLDatabaseConnection implements DatabaseConnection < public function query( . $args ) < // Исполняет запрос и возвращает результат. >> // Потребляющий код. $database = ( new Database )->get_connection(); $result = $database->query( $query );

Как видите, потребляющий код не так объёмен и несложен, только есть один нюанс. Мы решили называть фабрику Database вместо DatabaseConnection , так как это часть предоставляемого нами API, и мы всегда должны стремиться к балансу между логической точностью и элегантной краткостью.

Приведённая версия фабрики избавлена почти от всех ранее описанных недостатков, за одним исключением.

  • Мы убрали тесную взаимосвязь с объектом DatabaseConnection , но вместо этого создали новую, с фабрикой. Это не проблематично, потому что фабрика — чистая абстракция, вероятность того, что в какой-то момент понадобится отойти от концепции «инстанцирования», очень мала. Если это произойдёт, то, возможно, придётся пересмотреть всю парадигму ООП.

Вы, наверное, начинаете удивляться, что мы больше не можем принудительно ограничиваться единственным инстанцированием. Хотя мы всегда отдаём общий экземпляр реализации DatabaseConnection , кто угодно всё ещё может выполнить new MySOLDatabaseConnection и получить доступ к дополнительному экземпляру. Да, это так, и это одна из причин отказа от синглтона. Но это не всегда даёт преимущества в реальных задачах, поскольку делает невозможным соблюдение базовых требований вроде модульного тестирования.

Статичные заместители

Статичный заместитель (Static Proxy) — другой шаблон проектирования, на который можно поменять синглтон. Он подразумевает ещё более тесную связь, чем фабрика, но это хотя бы связь с абстракцией, а не с конкретной реализацией. Идея в том, что у вас есть статичное сопоставление (static mapping) интерфейса, и эти статичные вызовы прозрачно перенаправляются конкретной реализации. Таким образом, прямой связи с фактической реализацией нет, и статичный заместитель сам решает, как выбирать реализацию для использования.

// Статичный заместитель. final class Database < public static function get_connection(): DatabaseConnection < static $connection = null; if ( null === $connection ) < // You can have arbitrary logic in here to decide what // implementation to use. $connection = new MySQLDatabaseConnection(); >return $connection; > public static function query( . $args ) < // Forward call to actual implementation. self::get_connection()->query( . $args ); > > // Здесь у нас интерфейс, так что мы можем работать с несколькими реализациями и корректно имитировать (mock) ради тестирования. interface DatabaseConnection < public function query( . $args ); >// Используемая в данный момент реализация. final class MySQLDatabaseConnection implements DatabaseConnection < public function query( . $args ) < // Исполняется запрос и возвращается результат. >> // Потребляющий код. $result = Database::query( $query );

Как видите, статичный заместитель создаёт очень короткий и чистый API. К недостаткам можно отнести то, что возникает тесная связь кода с сигнатурой класса. При использовании в правильном месте особых проблем это не вызывает, так как это связь с абстракцией, которую можно контролировать напрямую, а не с конкретной реализацией. Вы всё ещё можете заменить код одной базы данных на код другой, который вы считаете нужным, а реализация всё ещё является совершенно нормальным объектом, который может быть протестирован.

API WordPress Plugin

API WordPress Plugin может заменить синглтоны, когда те используются ради возможности обеспечения глобального доступа через плагины. Это самое чистое решение с учётом ограничений WordPress’а, с оговоркой, что вся инфраструктура и архитектура вашего кода привязывается к API WordPress Plugin. Не применяйте этот способ, если вы собираетесь заново использовать ваш код в разных фреймворках.

// Здесь у нас интерфейс, так что мы можем работать с несколькими реализациями и корректно имитировать (mock) ради тестирования. interface DatabaseConnection < const FILTER = 'get_database_connection'; public function query( . $args ); >// Используемая в данный момент реализация. class MySQLDatabaseConnection implements DatabaseConnection < public function query( . $args ) < // Исполняется запрос и возвращается результат. >> // Инициализирующий код. $database = new MySQLDatabaseConnection(); add_filter( DatabaseConnection::FILTER, function () use ( $database ) < return $database; >); // Потребляющий код. $database = apply_filters( DatabaseConnection::FILTER ); $result = $database->query( $query );

Один из основных компромиссов состоит в том, что ваша архитектура напрямую привязана к API WordPress Plugin. Если вы планируете когда-либо предоставлять функциональность плагина для Drupal-сайтов, то код придётся полностью переписать.

Другая возможная проблема — теперь вы зависите от тайминга WordPress-перехватчиков (hooks). Это может привести к багам, связанным с таймингом, их зачастую трудно воспроизвести и исправить.

Service Locator

Локатор служб — это одна из форм контейнера инверсии управления (Inversion of Control Container). Некоторые сайты описывают метод как антишаблон. С одной стороны, это правда, но с другой, как мы уже обсуждали выше, все предложенные здесь рекомендации можно считать лишь компромиссами.

Локатор служб — это контейнер, который предоставляет доступ к службам, реализованным в других местах. Контейнер по большей части — это коллекция экземпляров, сопоставленных с идентификаторами. Более сложные реализации локатора служб могут привносить такие возможности, как ленивое инстанцирование или генерирование заместителей.

// Здесь у нас интерфейс контейнера, так что мы можем менять реализации локатора служб. interface Container < public function has( string $key ): bool; public function get( string $key ); >// Базовая реализация локатора служб. class ServiceLocator implements Container < protected $services = []; public function has( string $key ): bool < return array_key_exists( $key, $this->services ); > public function get( string $key ) < $service = $this->services[ $key ]; if ( is_callable( $service ) ) < $service = $service(); >return $service; > public function add( string $key, callable $service ) < $this->services[ $key ] = $service; > > // Здесь у нас интерфейс, так что мы можем работать с несколькими реализациями и корректно имитировать (mock) ради тестирования. interface DatabaseConnection < public function query( . $args ); >// Используемая в данный момент реализация. class MySQLDatabaseConnection implements DatabaseConnection < public function query( . $args ) < // Исполняется запрос и возвращается результат. >> // Инициализирующий код. $services = new ServiceLocator(); $services->add( 'Database', function () < return new MySQLDatabaseConnection(); >); // Потребляющий код. $result = $services->get( 'Database' )->query( $query );

Как вы уже могли догадаться, проблема получения ссылки на экземпляр $services не пропала. Её можно решить, объединив этот метод с любым из предыдущих трёх.

$result = ( new ServiceLocator() )->get( 'Database' )->query( $query );
$result = Services::get( 'Database' )->query( $query );
$services = apply_filters( 'get_service_locator' ); $result = $services->get( 'Database' )->query( $query );

Однако всё ещё нет ответа на вопрос, нужно ли пользоваться антишаблоном локатор служб вместо антишаблона синглтон… С локатором служб связана проблема: он «прячет» зависимости. Представим кодовую базу, которая использует правильное внедрение конструктора. В таком случае достаточно взглянуть на конструктор конкретного объекта, и можно сразу понять, от какого объекта он зависит. Если объект имеет доступ к ссылке на локатор служб, то вы можете обойти это явное разрешение зависимостей и извлечь ссылку (а следовательно, начать зависеть) на любой объект из реальной логики. Вот что имеют в виду, когда говорят, что локатор служб «прячет» зависимости.

Но, учитывая контекст WordPress, мы должны принять тот факт, что с самого начала нам недоступно идеальное решение. Нет технических возможностей реализовать правильное внедрение зависимостей по всей кодовой базе. Это значит, что нам в любом случае придётся искать компромисс. Локатор служб — не идеальное решение, однако этот шаблон хорошо укладывается в легаси-контекст и как минимум позволяет вам собрать все «компромиссы» в одном месте, а не раскидывать их по кодовой базе.

Внедрение зависимостей

Если вы работаете только в собственном плагине и вам не надо предоставлять доступ к своим объектам другим плагинам, то вам повезло: вы можете использовать настоящее внедрение зависимостей, чтобы избежать глобального доступа к зависимостям.

// Здесь у нас интерфейс, так что мы можем работать с несколькими реализациями и корректно имитировать (mock) ради тестирования. interface DatabaseConnection < public function query( . $args ); >// Используемая в данный момент реализация. class MySQLDatabaseConnection implements DatabaseConnection < public function query( . $args ) < // Исполняется запрос и возвращается результат. >> // Для демонстрации идеи мы вынуждены смоделировать весь плагин. class Plugin < private $database; public function __construct( DatabaseConnection $database ) < $this->database = $database; > public function run() < $consumer = new Consumer( $this->database ); return $consumer->do_query(); > > // Потребляющий код. // Для демонстрации внедрения конструктора также смоделирован как целый класс. class Consumer < private $database; public function __construct( DatabaseConnection $database ) < $this->database = $database; > public function do_query() < // А вот настоящий потребляющий код. // В этом месте у нас внедрено произвольное подключение к базе данных. return $this->database->query( $query ); > > // Внедрение зависимости из загрузочного кода по всему дереву. $database = new MySQLDatabaseConnection(); $plugin = new Plugin( $database ); $result = $plugin->run();

Выглядит немного сложнее, но имейте в виду, что мы должны были написать базовую версию всего плагина, чтобы продемонстрировать внедрение зависимостей.

Хотя у нас не может быть полного внедрения зависимостей во всём приложении, но мы, по крайней мере, можем получить его с ограничениями в плагине.

Это пример того, как всё соединить (wiring) вручную с помощью явного инстанцирования самих зависимостей. В более сложной кодовой базе вам захочется использовать автосоединяющееся внедрение зависимостей (Dependency Injector) (специализированный контейнер), которое принимает предварительную информацию о конфигурации и может рекурсивно инстанцировать целое дерево за один вызов.

Вот пример того, как можно сделать это соединение с помощью такого внедрения зависимостей (даны те же классы/интерфейсы, как и в предыдущем примере):

// Позволяет внедрению узнать, какую реализацию использовать для разрешения (resolving) интерфейса DatabaseConnection. $injector->alias( DatabaseConnection::class, MySQLDatabaseConnection::class ); // Позволяет внедрению узнать, что на запросы DatabaseConnection оно всегда должно возвращать один и тот же общий экземпляр. $injector->share( DatabaseConnection::class ); // Позволяет внедрению инстанцировать класс Plugin, который заставит его рекурсивно обойти все конструкторы и инстанцировать объекты, чтобы решить зависимости. $plugin = $injector->make( Plugin::class );

Комбинации

Для более сложных потребностей, которые разбросаны по нескольким пользовательским и сторонним плагинам, рассмотрите способы комбинирования рассмотренных подходов, которые не будут противоречить вашим условиям.

Например, в более сложных проектах я использовал следующий подход, чтобы приблизиться к идеальному решению, который описано выше:

  • Каждый плагин инстанцирован с помощью централизованного автосоединяющегося внедрения зависимостей.
  • Каждый плагин — это поставщик услуг (service provider), который может регистрировать службы с помощью централизованного локатора службы.
  • Зависимости внутри плагина —> внедрение зависимостей.
  • Зависимости между плагинами —> обнаружение служб (service location).
  • Сторонние зависимости —> виртуальные службы, в которые обёрнута сторонняя функциональность.

На странице Bright Nucleus Architecture вы можете почитать об этом подходе и посмотреть записи.

Заключение

Несколько возможных подходов позволяют избавиться от синглтонов. Хотя ни один из них не идеален в контексте WordPress’а, они все без исключения предпочтительней синглтона.

Помните, что связанные с синглтонами проблемы заключаются не в том, что те расшарены, а в том, что синглтоны заставляют инстанцировать их самих.

Если вы знаете ситуации, когда синглтон — единственное подходящее решение, пишите в комментариях!

  • php
  • design patterns
  • singleton
  • никто не читает теги

Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью

Обложка поста Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью

Паттерн «Одиночка» — пожалуй, самый известный паттерн проектирования. Тем не менее, он не лишен недостатков, поэтому некоторые программисты (например, Егор Бугаенко) считают его антипаттерном. Разбираемся в том, какие же подводные камни таятся в Singleton’е.

Определение паттерна

Само описание паттерна достаточно простое — класс должен гарантированно иметь лишь один объект, и к этому объекту должен быть предоставлен глобальный доступ. Скорее всего, причина его популярности как раз и кроется в этой простоте — всего лишь один класс, ничего сложного. Это, наверное, самый простой для изучения и реализации паттерн. Если вы встретите человека, который только что узнал о существовании паттернов проектирования, можете быть уверены, что он уже знает про Singleton. Проблема заключается в том, что когда из инструментов у вас есть только молоток, всё вокруг выглядит как гвозди. Из-за этого «Одиночкой» часто злоупотребляют.

Простейшая реализация

Как уже говорилось выше, в этом нет ничего сложного:

  • Сделайте конструктор класса приватным, чтобы не было возможности создать экземпляр класса извне.
  • Храните экземпляр класса в private static поле.
  • Предоставьте метод, который будет давать доступ к этому объекту.
public class Singleton < private static Singleton instance = new Singleton(); private Singleton() < >public static Singleton getInstance() < return instance; >> 

Принцип единственной обязанности

В объектно-ориентированном программировании существует правило хорошего тона — «Принцип едиственной обязанности» (Single Responsibility Principle, первая буква в аббревиатуре SOLID). Согласно этому правилу, каждый класс должен отвечать лишь за один какой-то аспект. Совершенно очевидно, что любой Singleton-класс отвечает сразу за две вещи: за то, что класс имеет лишь один объект, и за реализацию того, для чего этот класс вообще был создан.

Принцип единственной обязанности был создан не просто так — если класс отвечает за несколько действий, то, внося изменения в один аспект поведения класса, можно затронуть и другой, что может сильно усложнить разработку. Так же разработку усложняет тот факт, что переиспользование (reusability) класса практически невозможно. Поэтому хорошим шагом было бы, во-первых, вынести отслеживание того, является ли экземпляр класса единственным, из класса куда-либо во вне, а во-вторых, сделать так, чтобы у класса, в зависимости от контекста, появилась возможность перестать быть Singleton’ом, что позволило бы использовать его в разных ситуациях, в зависимости от необходимости (т.е. с одним экземпляром, с неограниченным количество экземпляров, с ограниченным набором экземпляров и так далее).

Тестирование

Один из главных минусов паттерна «Одиночка» — он сильно затрудняет юнит-тестирование. «Одиночка» привносит в программу глобальное состояние, поэтому вы не можете просто взять и изолировать классы, которые полагаются на Singleton. Поэтому, если вы хотите протестировать какой-то класс, то вы обязаны вместе с ним тестировать и Singleton, но это ещё полбеды. Состояние «Одиночки» может меняться, что порождает следующие проблемы:

  • Порядок тестов теперь имеет значение;
  • Тесты могут иметь нежелательные сторонние эффекты, порождённые Singleton’ом;
  • Вы не можете запускать несколько тестов параллельно;
  • Несколько вызовов одного и того же теста могут приводить к разным результатам.

На эту тему есть отличный доклад с «Google Tech Talks»:

Скрытые зависимости

Обычно, если классу нужно что-то для работы, это сразу понятно из его методов и конструкторов. Когда очевидно, какие зависимости есть у класса, гораздо проще их предоставить. Более того, в таком случае вы можете использовать вместо реально необходимых зависимостей заглушки для тестирования. Если же класс использует Singleton, это может быть совершенно не очевидно. Всё становится гораздо хуже, если экземпляру класса для работы необходима определённая инициализация (например, вызов метода init(. ) или вроде того). Ещё хуже, если у вас существует несколько Singleton’ов, которые должны быть созданы и инициализированы в определённом порядке.

Загрузчик класса

Если говорить о Java, то обеспечение существования лишь одного экземпляра класса, которое так необходимо для Singleton, становится всё сложнее. Проблема в том, что классическая реализация не проверяет, существует ли один экземпляр на JVM, он лишь удостоверяется, что существует один экземпляр на classloader. Если вы пишете небольшое клиентское приложение, в котором используется лишь один classloader, то никаких проблем не возникнет. Однако если вы используете несколько загрузчиков класса или ваше приложение должно работать на сервере (где может быть запущено несколько экземпляров приложения в разных загрузчиках классов), то всё становится очень печально.

Десериализация

Ещё один интересный момент заключается в том, что на самом деле стандартная реализация Singleton не запрещает создавать новые объекты. Она запрещает создавать новые объекты через конструктор. А ведь существуют и другие способы создать экземпляр класса, и один из них — сериализация и десериализация. Полной защиты от намеренного создания второго экземпляра Singleton’а можно добиться только с помощью использования enum’а с единственным состоянием, но это — неоправданное злоупотребление возможностями языка, ведь очевидно, что enum был придуман не для этого.

Потоконебезопасность

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

public static Singleton getInstance() < if (instance == null) < instance = new Singleton(); >return instance; > 

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

  • Первый поток обращается к getInstance() , когда объект ещё не создан;
  • В это время второй тоже обращается к этому методу, пока первый ещё не успел создать объект, и сам создаёт его;
  • Первый поток создаёт ещё один, второй, экземпляр класса.

Разумеется, можно просто пометить метод как synchronised , и эта проблема исчезнет. Проблема заключается в том, что, сохраняя время на старте программы, мы теперь будем терять его каждый раз при обращении к Singleton’у из-за того, что метод синхронизирован, а это очень дорого, если к экземпляру приходится часто обращаться. А ведь единственный раз, когда свойство synchronised действительно требуется — первое обращение к методу.

Есть два способа решить эту проблему. Первый — пометить как synchronised не весь метод, а только блок, где создаётся объект:

public static Singleton getInstance() < if (instance == null) < synchronized (Singleton.class) < if (instance == null) < instance = new Singleton(); >> > return instance; > 

Не забывайте, что это нельзя использовать в версии Java ниже, чем 1.5, потому что там используется иная модель памяти. Также не забудьте пометить поле instance как volatile .

Второй путь — использовать паттерн «Lazy Initialization Holder». Это решение основано на том, что вложенные классы не инициализируются до первого их использования (как раз то, что нам нужно):

public class Singleton < private Singleton() < >public static Singleton getInstance() < return SingletonHolder.instance; >private static class SingletonHolder < private static final Singleton instance = new Singleton(); >> 

Рефлексия

Мы запрещаем создавать несколько экземпляров класса, помечая конструктор приватным. Тем не менее, используя рефлексию, можно без особого труда изменить видимость конструктора с private на public прямо во время исполнения:

Class clazz = Singleton.class; Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); 

Конечно, если вы используете Singleton только в своём приложении, переживать не о чем. А вот если вы разрабатываете модуль, который затем будет использоваться в сторонних приложениях, то из-за этого могут возникнуть проблемы. Какие именно, зависит от того, что делает ваш «Одиночка» — это могут быть как и риски, связанные с безопасностью, так и просто непредсказуемое поведение модуля.

Заключение

Несмотря на то, что паттерн Singleton очень известный и популярный, у него есть множество серьёзных недостатков. Чем дальше, тем больше этих недостатков выявляется, и оригинальные паттерны из книги GOF «Design Patterns» часто сегодня считаются антипаттернами. Тем не менее, сама идея иметь лишь один объект на класс по-прежнему имеет смысл, но достаточно сложно реализовать ее правильно.

Следите за новыми постами по любимым темам

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

Why Singleton Pattern is considered as anti-design pattern ?

We all have been using Singleton Pattern for a long time, but does it actually nominates to be a design pattern.

What is Singleton Pattern?

Singleton Pattern can be used when you need exactly one instance of the object. They are usually used for logging, database connections, caching.

How to write a Singleton Class?

1. Create a private constructor to restrict access from others.

2. Create a static method which will return the class instance.

Below is simple example of Singleton Class.

In software world everything comes with pros and cons. In case of Singleton Design pattern the list of cons is quite long.

Because of the long list of negative points Singleton Design pattern is also considered as anti-design pattern.

Lets talk about the disadvantages:

1. Violating Single Responsibility Principle :

Single Responsibility Principle states that every class should have a single task to do.

In case of Singleton class it will have two responsibility one to create an instance and other to do the actual task.

2. Breaks the Open Closed Principle :

Open Closed Principle can be explained in a single statement “Open for Extension closed for Modification”.

Singleton class always returns its own instance and is never open for extension.

3. Difficult to Test :

Singleton class have a global state for there instance. Programs that have global state hide there dependencies and makes it difficult to test.

4. Dependency Inversion Violation :

Dependency Inversion Principle ensures that change in low level details should not impact the high level abstraction.

Any low level changes in singleton pattern system we need to do change the Singleton class.

5. Tight Coupling :

There will be number of classes calling the Singleton class instance.

Any change in Singleton class will impact all other classes, this results into tight coupling.

Tight coupling is always harmful for system, any small change you can need to change all the classes implementing it, change all the test classes.

Шаблон проектирования Singleton

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

Если абстрагироваться от этого скучного термина, взятого из Википедии, можно привести понятный, но немного грубый пример из реальной жизни: в стране может быть лишь один президент, этот президент действует по обстоятельствам и, в каком-то смысле, этот президент и является одиночкой. Скажем так, шаблон обеспечивает, что создаваемый объект — это единственный объект своего класса.

Прежде чем продолжить, скажем, что этот шаблон не стоит чрезмерно использовать. Все дело в том, что Singleton признан антипаттерном, однако это не значит, что он обязательно всегда плох. Порой, он может быть полезным, однако применять его следует с осторожностью. Он вводит в ваше приложение глобальное состояние, в результате чего его изменение в одном месте способно повлиять и на другие части разрабатываемого приложения, а это уже может вызвать трудности процесса отладки. И второй минус — Singleton делает ваш код связанным, то есть подводные камни-таки имеются.

Но давайте лучше перейдем к коду. Для создания «Одиночки» следует сделать конструктор приватным, отключить клонирование и расширение, плюс создать статическую переменную, необходимую для хранения экземпляра:

Screenshot_1-1801-9a7330.png

А вот и пример использования:

Screenshot_2-1801-14918b.png

Если интересует, есть реализация и на Java.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *