Каждый раз при обсуждении программного обеспечения с другими разработчиками всплывает тема синглтонов, особенно в контексте развития 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 );
  • С API WordPress Plugin:
    $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’а, они все без исключения предпочтительней синглтона.


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


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

Поделиться с друзьями
-->

Комментарии (55)


  1. Renius
    26.07.2017 15:13
    +8

    S — принцип единственной ответственности. Очевидно, что синглтон противоречит ему, как уже говорилось раньше.


    То что вы накладываете на синглтон ответственность по управлению инстансами не значит, что во всех языках синглтоны такие. Может быть это проблема реализации, а не синглтона как такового?

    Синглтону God в монотеизме не нужно управлять инстансами. Их убивают в религиозных войнах без участия God.

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

    Этот принцип не применим, так как не обязан возвращать расширение по определению.

    L — принцип подстановки Барбары Лисков: объекты могут быть заменены экземплярами своих подтипов без изменения использующего их кода. Это неверно в случае с синглтоном, потому что наличие нескольких разных версий объекта означает, что это уже не синглтон.
    Для таких объектов нельзя сказать, что такие объекты реализую синглтон. Если они не реализуют синглтон, то синглтон не нарушает принцип.
    I — принцип разделения интерфейса: много специализированных интерфейсов лучше, чем один универсальный. Это единственный принцип, который синглтон нарушает не напрямую, но лишь потому, что он не позволяет использовать интерфейс.

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

    И снова…

    Ну я не знаю… Не убедили…


    1. Bonart
      27.07.2017 02:26
      -3

      Может быть это проблема реализации, а не синглтона как такового?

      Вы банду четырех читали? Это проблема синглтона как такового по определению. Он делает что-то полезное (1) и контролирует число своих экземпляров (2).


      Ну я не знаю… Не убедили…

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


      1. MacIn
        27.07.2017 03:57
        +3

        И «принцип А неприменим для синглтона» — это в точности то же самое, что и «синглтон нарушет принцип А».

        Это совершенно разные фразы, а не «в точности то же самое».


      1. MacIn
        27.07.2017 04:15
        +7

        Вы банду четырех читали? Это проблема синглтона как такового по определению. Он делает что-то полезное (1) и контролирует число своих экземпляров (2).

        Это в общем случае тоже некорректно.
        «Синглтон» — это, на минутку, шаблон проектирования. Шаблон по определению не может делать что-то полезное или что-то контролировать. Он лишь рамки, алгоритм, общие принципы, по которым будет написан участок кода.
        Если мы хотим, чтобы некий класс X инстанцировался ровно единожды, и применяем шаблон синглтон для этого, то делать что-то полезное будет новый инстанс этого класса. А вот контроллировать число может и не класс X. Это может быть другой класс со статичным методом или вовсе функция, если язык позволяет. Заниматься «чем-то полезным» будет instance класса, создавать его (ровно единожды) — совсем другая сущность. И каждая из них будет иметь ровно single responsibility. А в целом это будет по-прежнему шаблон синглтон.


  1. AlexLeonov
    26.07.2017 15:56
    -2

    Прямо религиозный текст:
    «И сказал Исаак Иакову: не используй синглтоны, они плохо тестируются! И стало так!»

    Предпочитаю не слушать проповедников.


    1. Bonart
      27.07.2017 02:31
      -2

      Текст очень конкретный и легко проверяемый.
      А вот ваш комментарий — религиозный. В нем только оценка статьи как еретической и ни одного аргумента по существу.
      Хотя было бы достаточно всего лишь одного примера синглтона, более простого и полезного, чем альтернативы.


  1. MacIn
    26.07.2017 16:08
    +2

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


    Это весьма странная претензия. Сиглтон используется тогда, когда объект, который им инстанцируется, максимально универсален. Если нам при вызове из точки А надо настраивать объект (передавая параметры в коснтруктор) вот так, а при вызове из точки Б — иначе, то синглтон здесь неприменим, нам надо 2 инстанса с разными «настройками».

    Вы не можете имитировать (mock away) синглтон при тестировании компонентов, использующих его

    Это зависит от конкретного языка и фреймворка для тестирования.

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

    Потому что сиглтон-объект не должен иметь не-потокобезопасного внутреннего state'а, ни побочных эффектов.

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

    Точно так же будет и с не-синглтоном: вам придется написать какой-нибудь ConnectionList или ConnectionPool, и получать из него конкретный DatabaseConnection, который будет «синглтоном», или обычным объектом в зависимости от конкретных требований.
    Это опять же странная претензия — «если нам потребуется что-то сильно изменить, нам надо будет что-то сильно изменить».


    1. Bonart
      27.07.2017 02:47

      Сиглтон используется тогда, когда объект, который им инстанцируется, максимально универсален

      Универсальный объект, не позволяющий себя параметризовать самым простым способом? Ограничивающий число экземпляров единицей? С требованиями по отсутствию состояния и многопоточной инициализации?
      Это полная противоположность универсальности.


      Это опять же странная претензия — «если нам потребуется что-то сильно изменить, нам надо будет что-то сильно изменить».

      Видите ли, при использовании DI ничего сильно изменять не придется. Использование разных экземпляров в разных контекстах само по себе требует сильных изменений только в легаси или в коде недостаточно квалифицированного разработчика.


      Это зависит от конкретного языка и фреймворка для тестирования

      monkey patch ради тестов — это тоже антипаттерн. С параметрами конструктора такой проблемы нет в принципе.


      1. MacIn
        27.07.2017 03:42
        +1

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

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

        С требованиями по отсутствию состояния и многопоточной инициализации?

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

        Видите ли, при использовании DI ничего сильно изменять не придется. Использование разных экземпляров в разных контекстах само по себе требует сильных изменений только в легаси или в коде недостаточно квалифицированного разработчика.

        Это относится к разработке самого объекта, а не к синглтону как таковому.

        monkey patch ради тестов — это тоже антипаттерн. С параметрами конструктора такой проблемы нет в принципе.

        Во-первых, я не согласен с тем, что это антипаттерн. Во-вторых, почему monkey patch? Обычный mock можно применить и для тестирования синглтона. Зависит от конкретного языка.


  1. tehSLy
    26.07.2017 16:14
    +3

    Я возможно сейчас хапану негатива в свой адрес, но я уже устал молчать в неведении: в чем принципиальная разница(возьмем в пример PHP) между инстансом глобального объекта, и например классом со статическими переменными и методами?


    1. MacIn
      26.07.2017 16:28
      +3

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


      1. tehSLy
        26.07.2017 17:44

        Спасибо за ответ. Теперь у меня в голове стало немного светлее :)


    1. msts2017
      26.07.2017 17:06

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


  1. TheShock
    26.07.2017 16:17
    +3

    Ваша фабрика со статикой, по сути, тот же синглтон разделенный на два класса. Да, я понимаю, что он перестает нарушать принцип «S», но смысл? Все остальные недостатки остаются.

    $result = Database::query( $query );
    

    А тут вообще пошла процедурщина, пусть и на классах.

    В вашем Service Locator просто отвратительно использование строк, да и статическая типизация отваливается напрочь. В C#, конечно, можно было бы сделать это красиво благодаря Дженерикам. Что-то вроде такого:

    var conn = services.Get<IDatabaseConnection>();
    conn.query()
    


    Но очень мало языков позволяют такую красоту, а отказываться ради очевидных преимуществ статической типизации ради сомнительных преимуществ такого решения — сомнительный путь.

    Внедрение зависимостей — уже ближе к хорошей альтернативе, чем к плохой альтернативе, но непонятно, что делать в сложных приложениях с сильными зависимостями (в бизнес-логике). Вот представьте игрушку, вроде RimWorld. Активируешь какую-то абилку, а она одна влияет на настроение всех жителей мужского пола, на рост растений, а также на вероятность заспаунится металлических жуков, если прошло больше 3 лет с начала игры и на складе лежит не меньше, чем 1000 единиц металла и сложность игры на ниже «легкой». Как должен выглядеть конструктор такой абилки?

    class AwesomeAbility extends Ability {
      
      public CrewContainer crew;
      public PlantsManager plants;
      public EnemySpawner spawner;
      public GameTime time;
      public PlayerStorage storage;
      public GameConfig config;
      
      public AwesomeAbility (CrewContainer crew, PlantsManager plants, EnemySpawner spawner, GameTime time, PlayerStorage storage, GameConfig config) {
        this.crew = crew;
        this.plants = plants;
        this.spawner = spawner;
        this.time = time;
        this.storage = storage;
        this.config = config;
      }
    }
    


    А где все эти значения возьмет AwesomeAbilityFactory? Ее ведь тоже кто-то должен создать. А если вдруг еще понадобится влиять на животных? Во всей иерархии добавлять зависимость?

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

    class AwesomeAbility extends Ability {
      
      public CrewContainer crew;
      public PlantsManager plants;
      public EnemySpawner spawner;
      public GameTime time;
      public PlayerStorage storage;
      public GameConfig config;
      
      public AwesomeAbility (Game game) {
        this.crew = game.crew;
        this.plants = game.objects.plants;
        this.spawner = game.enemy.spawner;
        this.time = game.time;
        this.storage = game.player.storage;
        this.config = game.config;
      }
    }
    


    Это все-равно нарушение кучи принципов, но зато довольно юзабельно и статически типизированно.


    1. BoShurik
      26.07.2017 17:01

      Я бы через Event Dispatcher сделал:


      Как-то так
      <?php
      
      abstract class AbstractAwesomeAbilityListener
      {
          public function __construct(
              GameTime $time, 
              PlayerStorage $storage, 
              GameConfig $config
          )
          {
              // ...
          }
      
          abstract protected function doActivate();
      
          public function onActivate()
          {
              if (!$this->isAllowed()) {
                  return;
              }
      
              $this->doActivate();
          }
      
          /**
           * @return bool
           */
          protected function isAllowed()
          {
              // ...
          }
      }
      
      class CrewListener extends AbstractAwesomeAbilityListener
      {
          public function __construct(
              CrewContainer $container, 
              GameTime $time, 
              PlayerStorage $storage, 
              GameConfig $config
          )
          {
              parent::__construct($time, $storage, $config);
      
              // ...
          }
      
          protected function doActivate()
          {
              // ...
          }
      }
      
      class PlantsListener extends AbstractAwesomeAbilityListener
      {
          public function __construct(
              CrewContainer $container, 
              GameTime $time, 
              PlayerStorage $storage, 
              GameConfig $config
          )
          {
              parent::__construct($time, $storage, $config);
      
              // ...
          }
      
          protected function doActivate()
          {
              // ...
          }
      }
      
      class EnemyListener extends AbstractAwesomeAbilityListener
      {
          public function __construct(
              CrewContainer $container, 
              GameTime $time, 
              PlayerStorage $storage, 
              GameConfig $config
          )
          {
              parent::__construct($time, $storage, $config);
      
              // ...
          }
      
          protected function doActivate()
          {
              // ...
          }
      }


      1. TheShock
        26.07.2017 17:21

        Я не совсем понял, как вы предлагаете создавать эту комплексную абилку? Вместо того, чтобы создать одну AwesomeAbility — создать 5 подабилок? А что это даст?


        1. BoShurik
          26.07.2017 18:01
          +1

          Я не совсем понял, как вы предлагаете создавать эту комплексную абилку?

          $this->getEventDispatcher()->dispatch(Ability::AWESOME, new AwesomeAbilityEvent($subject, $target));

          Вместо того, чтобы создать одну AwesomeAbility — создать 5 подабилок? А что это даст?

          Это не пять абилок, это пять обработчиков одной абилки. Таким образом решаем проблему Single Responsibility. В вашем варианте у вас получается God-object


          1. TheShock
            26.07.2017 19:36

            AwesomeAbility в моем примере далека до God-object, хотя она и правда имеет многовато обязаностей. Но слишком много обязаностей не всегда означатает God-object. Божественный объект — это что-то вроде класса GameController, где описаны и создание цветов и все абилки и так далее.

            Декомпозировать можно по разному, в том числе полегче:

            class AwesomeAbility extends ComplexAbility {
            
              public Ability[] CreateParts () {
                return {
                  new CrewSubAbility(),
                  new PlantsSubAbility(),
                  // ...
                };
              }
              
            }
            


            Но мы ведь говорим сейчас не так о декомпозиции, как о работе с зависимостями. У вас вручную создаются громоздкие конструкторы в вашем классе. И непонятно, кто именно будет создавать все эти листенеры и в них передавать зависимости. Так же непонятно, где именно вы хотите хранить стейт для абилки, если у вас она столь разбита. Например, если отключение абилки завязано на время или какой-то тригер. Скажем, абилка работает три дня, или пока не нажмут рычаг. А если абилка может навешиваться несколько раз? Ну классическая баф эффект с рандомным значением (помните, как в СтарКрафте маринерам много раз нажимали шприц, чтобы повысить статы?):

            Вот представьте, вы юнитам добавляете
            unit.AddAbility( new AttackIncrease(Random(3, 10), Seconds(Random(10, 20))) )
            unit.AddAbility( new AttackIncrease(Random(3, 10), Seconds(Random(10, 20))) )
            unit.AddAbility( new AttackIncrease(Random(3, 10), Seconds(Random(10, 20))) )
            


            И вот, через разное время должна слететь одна, потом вторая, потом третья абилка.

            Кстати, еще абстрактный эвент плохое решение, потому что, к примеру, необходимо отображать, какие же абилки висят на юните. И, чем меньше времени ей осталось действовать — тем более прозрачной она должна рендерится. У вас вместо одной супер-абилити будет множество эвентов, которые просто влияют на юнит. Даже в клиент-серверной архитектуре — серверу нужно будет отдавать что-то вроде

            {
              unitId: 123,
              effects: [
                { ability: "AwesomeAbility", timeLeft: 24, power: 12 },
                { ability: "AwesomeAbility", timeLeft: 14, power: 7  },
                { ability: "AwesomeAbility", timeLeft: 28, power: 29 },
              ]
            }
            


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

            Кстати, иногда абилки могут складываться из других абилок, которые могут работать сами по себе. Скажем, абилка «Увеличьте атаку на 2 за счет уменьшения прочности в два раза» может складываться из двух уже существующих абилок «Увеличение атаки на Х» и «Уменьшение прочности в Х раз», тут тоже удобен подход с ComplexAbility


            1. BoShurik
              27.07.2017 12:07

              AwesomeAbility в моем примере далека до God-object, хотя она и правда имеет многовато обязаностей. Но слишком много обязаностей не всегда означатает God-object.

              Если сейчас он таким не является, то в будущем — весьма вероятно, "если вдруг еще понадобится влиять на животных"


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

              DI контейнер. Если говорить о PHP и Symfony в частности, то даже конфигурировать почти не придется. Думаю в других языках есть аналоги.


              Стейт и отображение решил бы так:


              Код
              class Unit
              {
                  private $abilites;
              
                  public function activateStimPack(EventDispatcher $dispatcher)
                  {
                      $ability = new StimPackAbility($this, 10 /* seconds */);
                      $dispatcher->dispatch(Abilities::STIM_PACK_ACTIVATE, $ability);
                  }
              
                  public function deactivateStimPacks(EventDispatcher $dispatcher)
                  {
                      $abilities = $this->getAbilities();
                      foreach ($abilities as $ability) {
                          $dispatcher->dispatch(Abilities::STIM_PACK_DEACTIVATE, $ability);
                      }
                  }
              }
              
              class StimPackListener
              {
                  public function __construct(EventDispatcher $dispatcher, GameTimer $timer)
                  {
              
                  }
              
                  public function onActivate(StimPackAbility $ability)
                  {
                      $ability->getUnit()->addAbility($ability);
                      // Add other effects
                      $timer = $this->timer->tick(function() use ($ability){
                          $ability->decSeconds();
              
                          if ($ability->getSeconds() == 0) {
                              $this->dispatcher->dispatch(Abilities::STIM_PACK_DEACTIVATE, $ability);
                          }
                      });
              
                      $ability->setTimer($timer);
                  }
              
                  public function onDeactivate(StimPackAbility $ability)
                  {
                      $ability->getUnit()->removeAbility($ability);
                      // Remove other effects
                      $ability->getTimer()->destroy();
                  }
              }


              1. TheShock
                27.07.2017 12:41

                Если сейчас он таким не является, то в будущем — весьма вероятно, «если вдруг еще понадобится влиять на животных»

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

                На самом деле в вашем коде недостатки в том, что юнит должен иметь описание каждой абилки. Т.К. юнит — это просто айдишник + совокупность всех абилок (если передвижение — тоже абилка), то значительно выгоднее логику абилки хранить в коде абилки и, соответсвенно методы а-ля activateStimPack — лишние. Представьте сколько методов в юните должно быть для сотен абилок?

                Я тоже знаю, что мой подход не идеален. У меня логика абилок описывается в абилках (так как их очень много), а некоторые core-вещи даже дешевле описать в самих системах.

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


    1. Bonart
      27.07.2017 02:55
      -1

      Это плохой дизайн.
      Абилку и ее связи с конкретными характеристиками надо разделять.
      И сами характеристики вычислять по привязанным абилкам (калькуляторы само собой тоже выделить) вместо введения изменяемого состояния.


      1. TheShock
        27.07.2017 03:04
        +1

        Давайте с псевдокодом.


    1. mayorovp
      27.07.2017 14:11

      А где все эти значения возьмет AwesomeAbilityFactory? Ее ведь тоже кто-то должен создать.

      Фабрика в своем конструкторе примет либо эти значения, либо фабрики для них.


      1. TheShock
        27.07.2017 14:14

        А где возьмет их тот, кто создает фабрику?


        1. mayorovp
          27.07.2017 14:49

          Фабрика создается в Composition Root либо через IoC-контейнер, либо вручную. В первом случае детали реализации контейнера нам не интересны, во втором случае все зависимости доступны в виде локальных переменных.


          1. TheShock
            27.07.2017 15:10

            Фабрика создается в Composition Root

            Не многовато ли ответственности для Composition Root — создать по фабрике на каждую абилку и не только?


            1. mayorovp
              27.07.2017 15:11

              Нет, не многовато. Это одна ответственность — создание фабрик.


              1. TheShock
                27.07.2017 16:00

                Создание всех фабрик? Похоже на «у моего класса Game одна ответственность — чтобы игра работала».

                Меня всегда в Composition Root смущала перегруженность корня.


                1. Bonart
                  27.07.2017 20:01

                  В чем перегруженность?
                  Ответственность у точки сборки ровно одна: композиция объектов в контексте.
                  Контейнеры позволяют все настроить выразительно и гибко, сводя вопрос "созданря фабрики" к одной декларативной строке кода. Получается своего рода сборочный чертеж и автосборщик в одном флаконе.
                  Если точка сбоки становится слишком велика — выделяются вспомогательные контексты со своими точками сбоки и так далее.


  1. RomanPokrovskij
    26.07.2017 16:21

    Не правильно такими тяжелыми (специфическими) примерами иллюстрировать такой сильный (общий) тезис.
    Чем сингелтон отличается от «ссылки на функцию»? Ситуация: вы используете динамическую функцию зачем ее пересобирать каждый раз при переиспользовании?


    1. Bonart
      27.07.2017 02:57

      Ссылка на функцию, в отличие от синглтона, подменяется и параметризуется легко и приятно


      1. RomanPokrovskij
        31.07.2017 14:42

        Не понял про «параметризируется», поскольку в моем понимании «ссылка на функцию» и «ссылка на объект» это одно и тоже. Объект т.е. синглетон-объект это такая функция. Разве нет? Ну есть у объекта три метода, значит это функция возвращающая три метода.

        Не понял про «подменяется», поскольку синглетон можно передавать в функцию и явно, через параметры.
        Вообще это две разные проблемы: неявная передача параметров (может быть и не static индентификаторов даже) и объекты создаваемые при первом обращении и далее существующие в единственном числе в течении жизни аппликации (опять же могут быть и не через static определенные).


        1. Bonart
          31.07.2017 15:51
          +1

          Не понял про «параметризируется», поскольку в моем понимании «ссылка на функцию» и «ссылка на объект» это одно и тоже.

          У синглтона есть глобальная точка доступа по определению, а не просто какая-то "ссылка на объект". В результате синглтон отличается от ссылки на функцию чуть менее чем полностью.
          И само собой, функция может при каждом вызове возвращать ссылки на разные объекты.


          Не понял про «подменяется», поскольку синглетон можно передавать в функцию и явно, через параметры

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


          1. RomanPokrovskij
            31.07.2017 17:08

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

            Но в результате надеюсь я вас понял, вы против неявных параметров и за DI. Однако и тут мне не понятно противопоставление синглетона и DI, ведь любой IoC контейнер (т.е. реализация DI) можно сконфигурировать на возвращение синглетона.

            Не подумайте что я цепляюсь. Просто я стал понимать синглетон диалектически («нет ничего неизменяемого, но когда всё кругом переменные — очень не уютно»). И потому категорические суждения мне интересны, как возможность проверить собственную правоту.


            1. Bonart
              31.07.2017 18:13
              +2

              Не понимаю, что это значит, и что из этого следует, ведь ссылка на статический метод (или если хотите идентификатор статического метода, или идентификатор ссылки на метод) это тоже «глобальная точка доступа», да и не статический метод это тоже «глобальная точка доступа» (напр. в таблице виртуальных методов).

              Ссылка на статический (и экземплярный тоже) метод — это указатель на функцию или любой его эквивалент. Ее можно передать как параметр. И что именно будет вызываться по этой ссылке, определяется тем, кто передал параметр — т.е. это не глобальная точка доступа, а разрешенные кем-то другим зависимости.
              Глобальная точка доступа к синглтону — это возможность достать его откуда угодно, просто написав SomeSingleton.GetInstance()
              Эта точка доступа прямо прописана в определении паттерна "Синглтон" у банды четырех. И без запрета ее использования ни о какой подмене синглтона на что-то свое речь идти не может по определению. А с запретом — синглтона уже нет, тоже по определению.


              Однако и тут мне не понятно противопоставление синглетона и DI

              Синглтон самостоятельно ограничивает количество своих экземпляров.
              В DI количество экземпляров классов-компонентов ограничивает специальный объект: точка сборки (Composition Root)
              Как результат паттерн сигнлтон и DI несовместимы: чтобы использовать одно, придется отказаться от другого (в одном и том же контексте).


              ведь любой IoC контейнер (т.е. реализация DI) можно сконфигурировать на возвращение синглетона

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


              Просто я стал понимать синглетон диалектически

              А тут нечего понимать диалектически — это достаточно простая штука с однозначным определением.


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


              1. RomanPokrovskij
                31.07.2017 18:40

                Вы пишите: «цикл жизни „синглтон“ — не путать с паттерном!»… Такая позиция — позиция глухая. Одна сторона будет и далее использовать немутабильные, объекты/функции с циклом жизни синглтон (иногда даже давая им идентификаторы глобальной видимости времени компиляции), и называть их «синглтонами», а вы будете критиковать свои другие «синглтоны» (данные в определениях «банды четырех») и стороны не сойдутся.


                1. Bonart
                  31.07.2017 18:53

                  Одна сторона будет и далее использовать немутабильные, объекты/функции с циклом жизни синглтон (иногда даже давая им идентификаторы глобальной видимости времени компиляции), и называть их «синглтонами»,

                  В обсуждаемой статье предмет указан явно:


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

                  Так что "одной стороне" достаточно уметь и желать прочитать ровно то, что написано.


                  1. RomanPokrovskij
                    31.07.2017 19:19

                    Да как бы это ссылка на источник, а не определение. И определение у GoF если я правильно понял дается через описание. А в таком случае будут точки зрения что «плохой» синглетон в описании GoF является частным случаем «приличного» немутабильного, объекта/функции с циклом жизни «синглтон», доступного через глобальную область видимости во время компиляции (для удобства).


  1. ilyaplot
    26.07.2017 17:35

    Если ваш синглтон можно назвать антипаттерном, значит, вы неправильно применили синглтон или выбрали не тот паттерн. Проблема именно в том, что изучив синглтон, «программист» ставит галочку «паттерны» в резюме и начинает лепить синглтон везде, даже там, где ему не место. Возможно, стоит сказать «спасибо» HR'ам за то, что первый вопрос на собеседовании: «Что такое синглтон?».


    1. Bonart
      27.07.2017 03:01
      -1

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


  1. gonzazoid
    26.07.2017 18:20
    +2

    >В данной статье я попытаюсь раскрыть тему того, почему синглтоны никогда не должны использоваться в коде ТАКОГО ТО ЯЗЫКА ДЛЯ РЕШЕНИЯ ТАКИХ ТО ПРОБЛЕМ и какие есть альтернативы для решения ЭТИХ проблем.

    исправите на это — и да, у статьи появится смысл. В противном случае возникает вопрос — если по логике приложения у нее ДОЛЖЕН быть один глобальный объект — то что? Принципиально лепить его не синглтоном? Менять на логику в которой не нужен такой объект? Зачем?

    Простейший пример — в java enum реализованы через классы-синглтоны. Убрать такую реализацию? Зачем? Оно работает, быстро, шустро, с огоньком. Отказаться от перечислений и изменить синтаксис языка на такой в котором перечисления не нужны? Реализовать перечисления по другому? Зачем? Из принципа? Хорошо, тогда как? Если синглтон нехорош, то как реализовать без синглтона что бы в итоге было так же быстро и безопасно?

    ЛЮБОЙ из распространенных паттернов — это квинтэссенция чьего то s/кода/опыта/, и когда мы говорим об антипаттернах — надо понимать что это чей то опыт решения каких то задач и если мы считаем это антипаттерном, значит наши задачи другие, значит этот опыт нам не подходит. Но для этого надо видеть не только свой опыт и задачи но и опыт с задачами людей притащивших этот паттерн.


    1. DistortNeo
      26.07.2017 22:01
      -1

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


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


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


  1. Aleks_ja
    26.07.2017 19:39

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

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

    Проблему инициализации можно решить множеством способов. Мы решали немного нестандартно — при первом инклюде файла. Он же подключается автолоудом и только один раз.


    1. MacIn
      26.07.2017 22:49

      Проблему инициализации можно решить множеством способов. Мы решали немного нестандартно — при первом инклюде файла. Он же подключается автолоудом и только один раз.

      Это сильно завязано на особенности того или иного языка. А синглтон как паттерн — «интернационален».


  1. yury-dymov
    26.07.2017 20:28
    +1

    Это очень плохая статья.


    Можно дом простроить без молотка, но зачем? Синглтон — это точно такой же инструмент проектирование, как и другие. Если его использовать неправильно — будет нехорошо. Ну так это как молотком по пальцу заехать в первый раз и после этого обвинить инструмент в том, что это он такой плохой и лучше его никогда не использовать.


    Аргумент про SOLID не особо уместен, как уже написали выше. Про разные источники данных — в моделе я вообще не хочу знать, откуда у меня берутся данные, persistence разруливается на более низком уровне, поэтому это неудачный аргумент, если предположить, что у проекта нормальная архитектура. С тестированием проблема выглядит сильно надуманной, по крайней мере у нас нет проблем с тестированием синглтонов. Может быть это потому, что мы их проектируем соответствующе.


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


    Не хочется вешать ярлыки, но разработчик WordPress этой статьей лишний раз подтвердил, что там все очень плохо :/


  1. stanislav-belichenko
    26.07.2017 22:11

    Ну в общем-то автор пишет явно на определенную тематику (WP) и соответственно про определенный язык и определенные с этим всем причины.


  1. CrazyFizik
    26.07.2017 23:46
    -2

    Хм… стандартный шаблон препарирования любого… гхм… шаблона проектирования в ООП:-)
    1. Выбираем любой рэндомный паттерн.
    2. Обнаруживаем, что он нарушает все возможные принципы ООП
    3. Пытаемся подобрать альтернативу
    4. Обнаруживаем что альтернатива нарушает все возможные принципы ООП
    5. Повторяем до тех пор пока не обнаружим серебряную пулю…

    Вообще наверное уже давно пора смирится с мыслью, шо все эти ваши ООП — просто удачная мистификация, а SOLID — просто неудачная сферовакуумная академическая абстракция натянутая Маритном на глобус. Не, невареное на рубеже 60-70 годов это действительно было революционно и окрыляющее на фон Алгола и Фортрана, но в академ-среде уже давно разобрались что к чему и забыли, после чего продолжили дальше писать свои STL и прочие BLAS, а вот прикладные программисты вот уже 30 лет продолжают жрать кактус, колоться, плакать, но кактус не бросают. Забавно еще и то, что в итоге все равно получается код такой же фортрановский (ох ты же приподобный Котэ, первый язык программирования круче ассемблера), разве что почти всю математику выкинули — ибо на курсах двухнелельных погромистов некогда этим заниматься и перфокарты можно увидеть разве что в музее.


    1. modestguy
      27.07.2017 09:12

      А это уже (приводя аналогию с молотком выше) — ровно также, как если при строительстве дома использовать не победитовые свёрла, а камень и заточенный пруток. Зачем использовать то, что уже давно неэффективно(!) в данном моменте времени. Любой инструмент приносит свой профит на данном этапе развития… и нелогично использовать инструменты из прошлого, строя в настоящем дом. Меняются не только инструменты строительства, но и технология домов.

      По поводу синглтонов — уверен, что использовать этот паттерн нужно. Но подходить к нему с умом и понимать, когда и где его лучше применять. И никто не вправе указать вам СТРОГОЕ использование того или иного паттерна… Ровно как и того или иного инструмента при строительстве дома. Напомню, что паттерн — это всего лишь выделенный в границах шаблон, который повторяется у многих программистов для решения определённой задачи. Более того, большинство программистов, не знающих паттерны — используют их (или придут к их использованию) зачастую не зная, как они называются.


  1. retran
    27.07.2017 09:49
    +3

    Вот только Service Locator — это тоже антипаттерн и тоже синглтон со всеми его проблемами. Шел 2017 год…


  1. LireinCore
    27.07.2017 10:54
    +1

    DI-контейнеры полностью заменяют синглтоны, фабрики и локаторы


  1. nefone
    27.07.2017 11:45

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


    1. LireinCore
      27.07.2017 13:13
      +1

      Вот



  1. dubpub
    28.07.2017 13:38
    +2

    Странно, что так много комментирующих столь яро критикуют статью. Статья наглядно демонстрирует, что способов избежать использования синглтона более чем полно, особенно если заранее продумать всю архитектуру компонентов.
    Хотелось бы сказать в защиту статьи, что синглтон считается антипаттерном не потому, что он ломает SOLID или ещё какие принципы, не дающие спать занудам-перфекционистам, а потому что даёт возможность обращаться к этому единичному инстансу из любой точки кода. Это и создаёт неприятные моменты, как минимум в коде начинающих программистов: вместо минимизации обращений к объекту через метод MySingleton::getInstance() и передачи MySingleton $instance в качестве параметра в востребованный конструктор/метод, его сразу вызывают напрямую из любого места в коде, ведь так можно — это вроде как статика, да и паттерн это позволяет делать, — отсюда и вытекают основные сложности с тестированием, с тем, что это трудно "замокать"/подменить.


    1. mayorovp
      28.07.2017 13:46
      -1

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