Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Концептуальный пример
Этот пример показывает структуру паттерна Одиночка , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире 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!