Одиночка на PHP
Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Сложность:
Популярность:
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в PHP-коде.
Признаки применения паттерна: Одиночку можно определить по статическому создающему методу, который возвращает один и тот же объект.
Концептуальный пример
Этот пример показывает структуру паттерна Одиночка, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php namespace RefactoringGuru\Singleton\Conceptual; /** * Класс Одиночка предоставляет метод `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(): Singleton { $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(); Output.txt: Результат выполнения
Singleton works, both variables contain the same instance. Пример из реальной жизни
Паттерн Одиночка печально известен тем, что ограничивает повторное использование кода и усложняет модульное тестирование. Несмотря на это, он всё же очень полезен в некоторых случаях. В частности, он удобен, когда необходимо контролировать некоторые общие ресурсы. Например, глобальный объект логирования, который должен управлять доступом к файлу журнала. Еще один хороший пример: совместно используемое хранилище конфигурации среды выполнения.
index.php: Пример из реальной жизни
<?php namespace RefactoringGuru\Singleton\RealWorld; /** * Если вам необходимо поддерживать в приложении несколько типов Одиночек, вы * можете определить основные функции Одиночки в базовом классе, тогда как * фактическую бизнес-логику (например, ведение журнала) перенести в подклассы. */ 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(string $message): void { $date = date('Y-m-d'); fwrite($this->fileHandle, "$date: $message\n"); } /** * Просто удобный ярлык для уменьшения объёма кода, необходимого для * регистрации сообщений из клиентского кода. */ public static function log(string $message): void { $logger = static::getInstance(); $logger->writeLog($message); } } /** * Применение паттерна Одиночка в хранилище настроек – тоже обычная практика. * Часто требуется получить доступ к настройкам приложений из самых разных мест * программы. Одиночка предоставляет это удобство. */ class Config extends Singleton { private $hashmap = []; public function getValue(string $key): string { return $this->hashmap[$key]; } public function setValue(string $key, string $value): void { $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!"); Output.txt: Результат выполнения
2018-06-04: Started! 2018-06-04: Logger has a single instance. 2018-06-04: Config singleton also works fine. 2018-06-04: Finished!