- Суть паттерна
Одиночка - это порождающий паттерн проектирования, который гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.
- Проблема
Одиночка решает сразу две проблемы, нарушая принцип единственной ответственности класса.
1. Гарантирует наличие единственного экземпляра класса. Чаще всего это полезно для доступа к какому-то общему ресурсу, например, базе данных. Представьте, что вы создали объект, а через некоторое время пробуете создать ещё один. В этом случае хотелось бы получить старый объект, вместо создания нового. Такое поведение невозможно реализовать с помощью обычного конструктора, так как конструктор класса всегда возвращает новый объект.
2. Предоставляет глобальную точку доступа. Это не просто глобальная переменная, через которую можно достучаться к определённому объекту. Глобальные переменные не защищены от записи, поэтому любой код может подменять их значения без вашего ведома. Но есть и другой нюанс. Неплохо бы хранить в одном месте и код, который решает проблему №1, а также иметь к нему простой и доступный интерфейс.
Интересно, что в наше время паттерн стал настолько известен, что теперь люди называют "одиночками" даже те классы, которые решают лишь одну из проблем, перечисленных выше.
- Решение
Все реализации одиночки сводятся к тому, чтобы скрыть конструктор по умолчанию и создать публичный статический метод, который и будет контролировать жизненный цикл объекта-одиночки.
Если у вас есть доступ к классу одиночки, значит, будет доступ и к этому статическому методу. Из какой точки кода вы бы его ни вызвали, он всегда будет отдавать один и тот же объект.
- Структура
Одиночка определяет статический метод getInstance, который возвращает единственный экземпляр своего класса.
Конструктор одиночки должен быть скрыт от клиентов. Вызов метода getInstance должен стать единственным способом получить объект этого класса.
- Шаги реализации
- Преимущества
- Недостатки
Singleton - это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы. Singleton имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода. Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Концептуальный пример
/** * Класс Одиночка предоставляет метод `GetInstance`, который ведёт себя как * альтернативный конструктор и позволяет клиентам получать один и тот же * экземпляр класса при каждом вызове. */ class Singleton { /** * Объект одиночки храниться в статичном поле класса. Это поле — массив, так * как мы позволим нашему Одиночке иметь подклассы. Все элементы этого * массива будут экземплярами кокретных подклассов Одиночки. Не волнуйтесь, * мы вот-вот познакомимся с тем, как это работает. */ private static $instances = []; /** * Конструктор Одиночки всегда должен быть скрытым, чтобы предотвратить * создание объекта через оператор new. */ protected function __construct() { } /** * Одиночки не должны быть клонируемыми. */ protected function __clone() { } /** * Одиночки не должны быть восстанавливаемыми из строк. */ public function __wakeup() { throw new \Exception("Cannot unserialize a singleton."); } /** * Это статический метод, управляющий доступом к экземпляру одиночки. При * первом запуске, он создаёт экземпляр одиночки и помещает его в * статическое поле. При последующих запусках, он возвращает клиенту объект, * хранящийся в статическом поле. * * Эта реализация позволяет вам расширять класс Одиночки, сохраняя повсюду * только один экземпляр каждого подкласса. */ public static function getInstance() { $cls = static::class; if (!isset(self::$instances[$cls])) { self::$instances[$cls] = new static(); } return self::$instances[$cls]; } /** * Наконец, любой одиночка должен содержать некоторую бизнес-логику, которая * может быть выполнена на его экземпляре. */ public function someBusinessLogic() { // ... } } /** * Клиентский код. */ function clientCode() { $s1 = Singleton::getInstance(); $s2 = Singleton::getInstance(); if ($s1 === $s2) { echo "Singleton works, both variables contain the same instance."; } else { echo "Singleton failed, variables contain different instances."; } } clientCode();
Результат выполнения:
Singleton works, both variables contain the same instance.
Паттерн Singleton печально известен тем, что ограничивает повторное использование кода и усложняет модульное тестирование. Несмотря на это, он всё же очень полезен в некоторых случаях. В частности, он удобен, когда необходимо контролировать некоторые общие ресурсы. Например, глобальный объект логирования, который должен управлять доступом к файлу журнала. Еще один хороший пример: совместно используемое хранилище конфигурации среды выполнения.
Пример из реальной жизни
/** * Если вам необходимо поддерживать в приложении несколько типов Одиночек, вы * можете определить основные функции Одиночки в базовом классе, тогда как * фактическую бизнес-логику (например, ведение журнала) перенести в подклассы. */ class Singleton { /** * Реальный экземпляр одиночки почти всегда находится внутри статического * поля. В этом случае статическое поле является массивом, где каждый * подкласс Одиночки хранит свой собственный экземпляр. */ private static $instances = []; /** * Конструктор Одиночки не должен быть публичным. Однако он не может быть * приватным, если мы хотим разрешить создание подклассов. */ protected function __construct() { } /** * Клонирование и десериализация не разрешены для одиночек. */ protected function __clone() { } public function __wakeup() { throw new \Exception("Cannot unserialize singleton"); } /** * Метод, используемый для получения экземпляра Одиночки. */ public static function getInstance() { $subclass = static::class; if (!isset(self::$instances[$subclass])) { // Обратите внимание, что здесь мы используем ключевое слово // "static" вместо фактического имени класса. В этом контексте // ключевое слово "static" означает «имя текущего класса». Эта // особенность важна, потому что, когда метод вызывается в // подклассе, мы хотим, чтобы экземпляр этого подкласса был создан // здесь. self::$instances[$subclass] = new static(); } return self::$instances[$subclass]; } } /** * Класс ведения журнала является наиболее известным и похвальным использованием * паттерна Одиночка. */ class Logger extends Singleton { /** * Ресурс указателя файла журнала. */ private $fileHandle; /** * Поскольку конструктор Одиночки вызывается только один раз, постоянно * открыт всего лишь один файловый ресурс. * * Обратите внимание, что для простоты мы открываем здесь консольный поток * вместо фактического файла. */ protected function __construct() { $this->fileHandle = fopen('php://stdout', 'w'); } /** * Пишем запись в журнале в открытый файловый ресурс. */ public function writeLog($message) { $date = date('Y-m-d'); fwrite($this->fileHandle, "$date: $message\n"); } /** * Просто удобный ярлык для уменьшения объёма кода, необходимого для * регистрации сообщений из клиентского кода. */ public static function log($message) { $logger = static::getInstance(); $logger->writeLog($message); } } /** * Применение паттерна Одиночка в хранилище настроек – тоже обычная практика. * Часто требуется получить доступ к настройкам приложений из самых разных мест * программы. Одиночка предоставляет это удобство. */ class Config extends Singleton { private $hashmap = []; public function getValue($key) { return $this->hashmap[$key]; } public function setValue($key, $value) { $this->hashmap[$key] = $value; } } /** * Клиентский код. */ Logger::log("Started!"); // Сравниваем значения одиночки-Логгера. $l1 = Logger::getInstance(); $l2 = Logger::getInstance(); if ($l1 === $l2) { Logger::log("Logger has a single instance."); } else { Logger::log("Loggers are different."); } // Проверяем, как одиночка-Конфигурация сохраняет данные... $config1 = Config::getInstance(); $login = "test_login"; $password = "test_password"; $config1->setValue("login", $login); $config1->setValue("password", $password); // ...и восстанавливает их. $config2 = Config::getInstance(); if ( $login == $config2->getValue("login") && $password == $config2->getValue("password") ) { Logger::log("Config singleton also works fine."); } Logger::log("Finished!");
Результат выполнения:
2018-06-04: Started! 2018-06-04: Logger has a single instance. 2018-06-04: Config singleton also works fine. 2018-06-04: Finished!