Одиночка — это порождающий паттерн проектирования, который гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.
Проблема
Одиночка решает сразу две проблемы, нарушая принцип единственной ответственности класса.
Гарантирует наличие единственного экземпляра класса. Чаще всего это полезно для доступа к какому-то общему ресурсу, например, базе данных.
Представьте, что вы создали объект, а через некоторое время пробуете создать ещё один. В этом случае хотелось бы получить старый объект, вместо создания нового.
Такое поведение невозможно реализовать с помощью обычного конструктора, так как конструктор класса всегда возвращает новый объект.
Клиенты могут не подозревать, что работают с одним и тем же объектом.
Предоставляет глобальную точку доступа. Это не просто глобальная переменная, через которую можно достучаться к определённому объекту. Глобальные переменные не защищены от записи, поэтому любой код может подменять их значения без вашего ведома.
Но есть и другой нюанс. Неплохо бы хранить в одном месте и код, который решает проблему №1, а также иметь к нему простой и доступный интерфейс.
Интересно, что в наше время паттерн стал настолько известен, что теперь люди называют «одиночками» даже те классы, которые решают лишь одну из проблем, перечисленных выше.
Решение
Все реализации одиночки сводятся к тому, чтобы скрыть конструктор по умолчанию и создать публичный статический метод, который и будет контролировать жизненный цикл объекта-одиночки.
Если у вас есть доступ к классу одиночки, значит, будет доступ и к этому статическому методу. Из какой точки кода вы бы его ни вызвали, он всегда будет отдавать один и тот же объект.
Аналогия из жизни
Правительство государства — хороший пример одиночки. В государстве может быть только одно официальное правительство. Вне зависимости от того, кто конкретно заседает в правительстве, оно имеет глобальную точку доступа «Правительство страны N».
Структура
Одиночка определяет статический метод getInstance, который возвращает единственный экземпляр своего класса.
Конструктор одиночки должен быть скрыт от клиентов. Вызов метода getInstance должен стать единственным способом получить объект этого класса.
// Класс одиночки определяет статический метод `getInstance`,
// который позволяет клиентам повторно использовать одно и то же
// подключение к базе данных по всей программе.
class Database is
// Поле для хранения объекта-одиночки должно быть объявлено
// статичным.
private static field instance: Database
// Конструктор одиночки всегда должен оставаться приватным,
// чтобы клиенты не могли самостоятельно создавать
// экземпляры этого класса через оператор `new`.
private constructor Database() is
// Здесь может жить код инициализации подключения к
// серверу баз данных.
// ...
// Основной статический метод одиночки служит альтернативой
// конструктору и является точкой доступа к экземпляру этого
// класса.
public static method getInstance() is
if (Database.instance == null) then
acquireThreadLock() and then
// На всякий случай ещё раз проверим, не был ли
// объект создан другим потоком, пока текущий
// ждал освобождения блокировки.
if (Database.instance == null) then
Database.instance = new Database()
return Database.instance
// Наконец, любой класс одиночки должен иметь какую-то
// полезную функциональность, которую клиенты будут
// запускать через полученный объект одиночки.
public method query(sql) is
// Все запросы к базе данных будут проходить через этот
// метод. Поэтому имеет смысл поместить сюда какую-то
// логику кеширования.
// ...
class Application is
method main() is
Database foo = Database.getInstance()
foo.query("SELECT ...")
// ...
Database bar = Database.getInstance()
bar.query("SELECT ...")
// Переменная "bar" содержит тот же объект, что и
// переменная "foo".
Применимость
Когда в программе должен быть единственный экземпляр какого-то класса, доступный всем клиентам (например, общий доступ к базе данных из разных частей программы).
Одиночка скрывает от клиентов все способы создания нового объекта, кроме специального метода. Этот метод либо создаёт объект, либо отдаёт существующий объект, если он уже был создан.
Когда вам хочется иметь больше контроля над глобальными переменными.
В отличие от глобальных переменных, Одиночка гарантирует, что никакой другой код не заменит созданный экземпляр класса, поэтому вы всегда уверены в наличии лишь одного объекта-одиночки.
Тем не менее, в любой момент вы можете расширить это ограничение и позволить любое количество объектов-одиночек, поменяв код в одном месте (метод getInstance).
Шаги реализации
Добавьте в класс приватное статическое поле, которое будет содержать одиночный объект.
Объявите статический создающий метод, который будет использоваться для получения одиночки.
Добавьте «ленивую инициализацию» (создание объекта при первом вызове метода) в создающий метод одиночки.
Сделайте конструктор класса приватным.
В клиентском коде замените вызовы конструктора одиночка вызовами его создающего метода.
Преимущества и недостатки
Преимущества
Гарантирует наличие единственного экземпляра класса.
Требует постоянного создания Mock-объектов при юнит-тестировании.
Примеры
Python
Сложность: 1/3
Популярность: 2/3
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в Python-коде.
Признаки применения паттерна: Одиночку можно определить по статическому создающему методу, который возвращает один и тот же объект.
Топорно реализовать Одиночку очень просто — достаточно скрыть конструктор и предоставить статический создающий метод.
Тот же класс ведёт себя неправильно в многопоточной среде. Несколько потоков могут одновременно вызвать метод получения Одиночки и создать сразу несколько экземпляров объекта.
classSingletonMeta(type):""" В Python класс Одиночка можно реализовать по-разному. Возможные способы включают себя базовый класс, декоратор, метакласс. Мы воспользуемся метаклассом, поскольку он лучше всего подходит для этой цели. """_instances={}def__call__(cls,*args,**kwargs):""" Данная реализация не учитывает возможное изменение передаваемых аргументов в `__init__`. """ifclsnotincls._instances:instance=super().__call__(*args,**kwargs)cls._instances[cls]=instancereturncls._instances[cls]classSingleton(metaclass=SingletonMeta):defsome_business_logic(self):""" Наконец, любой одиночка должен содержать некоторую бизнес-логику, которая может быть выполнена на его экземпляре. """# ...if__name__=="__main__":# Клиентский код.s1=Singleton()s2=Singleton()ifid(s1)==id(s2):print("Singleton works, both variables contain the same instance.")else:print("Singleton failed, variables contain different instances.")
fromthreadingimportLock,ThreadclassSingletonMeta(type):"""Это потокобезопасная реализация класса Singleton."""_instances={}_lock:Lock=Lock()""" У нас теперь есть объект-блокировка для синхронизации потоков во время первого доступа к Одиночке. """def__call__(cls,*args,**kwargs):""" Данная реализация не учитывает возможное изменение передаваемых аргументов в `__init__`. """# Теперь представьте, что программа была только-только запущена.# Объекта-одиночки ещё никто не создавал, поэтому несколько потоков# вполне могли одновременно пройти через предыдущее условие и достигнуть# блокировки. Самый быстрый поток поставит блокировку и двинется внутрь# секции, пока другие будут здесь его ожидать.withcls._lock:# Первый поток достигает этого условия и проходит внутрь, создавая# объект-одиночку. Как только этот поток покинет секцию и освободит# блокировку, следующий поток может снова установить блокировку и# зайти внутрь. Однако теперь экземпляр одиночки уже будет создан и# поток не сможет пройти через это условие, а значит новый объект не# будет создан.ifclsnotincls._instances:instance=super().__call__(*args,**kwargs)cls._instances[cls]=instancereturncls._instances[cls]classSingleton(metaclass=SingletonMeta):value:str=None""" Мы используем это поле, чтобы доказать, что наш Одиночка действительно работает. """def__init__(self,value:str)->None:self.value=valuedefsome_business_logic(self):""" Наконец, любой одиночка должен содержать некоторую бизнес-логику, которая может быть выполнена на его экземпляре. """deftest_singleton(value:str)->None:singleton=Singleton(value)print(singleton.value)if__name__=="__main__":# Клиентский код.print("If you see the same value, then singleton was reused (yay!)\n""If you see different values, ""then 2 singletons were created (booo!!)\n\n""RESULT:\n")process1=Thread(target=test_singleton,args=("FOO",))process2=Thread(target=test_singleton,args=("BAR",))process1.start()process2.start()
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в PHP-коде.
Признаки применения паттерна: Одиночку можно определить по статическому создающему методу, который возвращает один и тот же объект.
Этот пример показывает структуру паттерна Одиночка, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
<?php/*** Класс Одиночка предоставляет метод `GetInstance`, который ведёт себя как* альтернативный конструктор и позволяет клиентам получать один и тот же* экземпляр класса при каждом вызове.*/classSingleton{/** * Объект одиночки храниться в статичном поле класса. Это поле — массив, так * как мы позволим нашему Одиночке иметь подклассы. Все элементы этого * массива будут экземплярами кокретных подклассов Одиночки. Не волнуйтесь, * мы вот-вот познакомимся с тем, как это работает. */privatestatic$instances=[];/** * Конструктор Одиночки всегда должен быть скрытым, чтобы предотвратить * создание объекта через оператор new. */protectedfunction__construct(){}/** * Одиночки не должны быть клонируемыми. */protectedfunction__clone(){}/** * Одиночки не должны быть восстанавливаемыми из строк. */publicfunction__wakeup(){thrownew\Exception("Cannot unserialize a singleton.");}/** * Это статический метод, управляющий доступом к экземпляру одиночки. При * первом запуске, он создаёт экземпляр одиночки и помещает его в * статическое поле. При последующих запусках, он возвращает клиенту объект, * хранящийся в статическом поле. * * Эта реализация позволяет вам расширять класс Одиночки, сохраняя повсюду * только один экземпляр каждого подкласса. */publicstaticfunctiongetInstance():Singleton{$cls=static::class;if(!isset(self::$instances[$cls])){self::$instances[$cls]=newstatic();}returnself::$instances[$cls];}/** * Наконец, любой одиночка должен содержать некоторую бизнес-логику, которая * может быть выполнена на его экземпляре. */publicfunctionsomeBusinessLogic(){// ...}}/*** Клиентский код.*/functionclientCode(){$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();
Паттерн Одиночка печально известен тем, что ограничивает повторное использование кода и усложняет модульное тестирование. Несмотря на это, он всё же очень полезен в некоторых случаях. В частности, он удобен, когда необходимо контролировать некоторые общие ресурсы. Например, глобальный объект логирования, который должен управлять доступом к файлу журнала. Еще один хороший пример: совместно используемое хранилище конфигурации среды выполнения.
<?php/*** Если вам необходимо поддерживать в приложении несколько типов Одиночек, вы* можете определить основные функции Одиночки в базовом классе, тогда как* фактическую бизнес-логику (например, ведение журнала) перенести в подклассы.*/classSingleton{/** * Реальный экземпляр одиночки почти всегда находится внутри статического * поля. В этом случае статическое поле является массивом, где каждый * подкласс Одиночки хранит свой собственный экземпляр. */privatestatic$instances=[];/** * Конструктор Одиночки не должен быть публичным. Однако он не может быть * приватным, если мы хотим разрешить создание подклассов. */protectedfunction__construct(){}/** * Клонирование и десериализация не разрешены для одиночек. */protectedfunction__clone(){}publicfunction__wakeup(){thrownew\Exception("Cannot unserialize singleton");}/** * Метод, используемый для получения экземпляра Одиночки. */publicstaticfunctiongetInstance(){$subclass=static::class;if(!isset(self::$instances[$subclass])){// Обратите внимание, что здесь мы используем ключевое слово// "static" вместо фактического имени класса. В этом контексте// ключевое слово "static" означает «имя текущего класса». Эта// особенность важна, потому что, когда метод вызывается в// подклассе, мы хотим, чтобы экземпляр этого подкласса был создан// здесь.self::$instances[$subclass]=newstatic();}returnself::$instances[$subclass];}}/*** Класс ведения журнала является наиболее известным и похвальным использованием* паттерна Одиночка.*/classLoggerextendsSingleton{/** * Ресурс указателя файла файла журнала. */private$fileHandle;/** * Поскольку конструктор Одиночки вызывается только один раз, постоянно * открыт всего лишь один файловый ресурс. * * Обратите внимание, что для простоты мы открываем здесь консольный поток * вместо фактического файла. */protectedfunction__construct(){$this->fileHandle=fopen('php://stdout','w');}/** * Пишем запись в журнале в открытый файловый ресурс. */publicfunctionwriteLog(string$message):void{$date=date('Y-m-d');fwrite($this->fileHandle,"$date: $message\n");}/** * Просто удобный ярлык для уменьшения объёма кода, необходимого для * регистрации сообщений из клиентского кода. */publicstaticfunctionlog(string$message):void{$logger=static::getInstance();$logger->writeLog($message);}}/*** Применение паттерна Одиночка в хранилище настроек – тоже обычная практика.* Часто требуется получить доступ к настройкам приложений из самых разных мест* программы. Одиночка предоставляет это удобство.*/classConfigextendsSingleton{private$hashmap=[];publicfunctiongetValue(string$key):string{return$this->hashmap[$key];}publicfunctionsetValue(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!");
Реализуйте ОдиночкуAppConfig, который загружает настройки из словаря при первом обращении и предоставляет доступ к ним через методы get(key) и set(key, value). Убедитесь, что в любой части программы используется один и тот же экземпляр, а повторное создание объекта невозможно.
# Без паттерна — конфигурация создаётся в каждом модуле зановоclassAppConfig:def__init__(self,settings:dict):self.settings=settingsdefget(self,key:str,default=None):returnself.settings.get(key,default)defset(self,key:str,value)->None:self.settings[key]=value# Проблема: в разных модулях создаются разные экземплярыconfig_in_module_a=AppConfig({"debug":False,"db_host":"localhost","db_port":5432})config_in_module_b=AppConfig({"debug":False,"db_host":"localhost","db_port":5432})config_in_module_a.set("debug",True)# module_b не видит изменения module_a — это разные объекты!print(config_in_module_b.get("debug"))# False, а должно быть True# TODO: переделать AppConfig в Одиночку так, чтобы:# - AppConfig() или AppConfig.get_instance() всегда возвращал один объект# - настройки инициализировались только один раз# - изменения были видны из любого модуля
<?php// Без паттерна — конфигурация создаётся в каждом модуле зановоclassAppConfig{privatearray$settings;publicfunction__construct(array$settings){$this->settings=$settings;}publicfunctionget(string$key,mixed$default=null):mixed{return$this->settings[$key]??$default;}publicfunctionset(string$key,mixed$value):void{$this->settings[$key]=$value;}}// Проблема: в разных модулях создаются разные экземпляры$configInModuleA=newAppConfig(['debug'=>false,'db_host'=>'localhost']);$configInModuleB=newAppConfig(['debug'=>false,'db_host'=>'localhost']);$configInModuleA->set('debug',true);// module_b не видит изменения module_a — это разные объекты!var_dump($configInModuleB->get('debug'));// false, а должно быть true// TODO: переделать AppConfig в Одиночку так, чтобы:// - AppConfig::getInstance() всегда возвращал один объект// - настройки инициализировались только один раз// - изменения были видны из любого места программы
Задача 2. Пул соединений с базой данных
Реализуйте ОдиночкуConnectionPool, который управляет ограниченным набором соединений с базой данных. Пул должен создавать соединения только при необходимости (ленивая инициализация), выдавать свободное соединение методом acquire() и принимать его обратно через release(). Убедитесь, что максимальное количество одновременных соединений не превышает заданного лимита.
importthreadingclassDatabaseConnection:_id_counter=0def__init__(self):DatabaseConnection._id_counter+=1self.id=DatabaseConnection._id_counterself.in_use=Falsedefexecute(self,query:str)->str:returnf"[Conn #{self.id}] Результат: {query}"def__str__(self)->str:returnf"Connection #{self.id}"# TODO: реализовать Одиночку ConnectionPool:# - __init__(max_size: int = 5) — вызывается только один раз# - acquire() -> DatabaseConnection — возвращает свободное соединение# или бросает RuntimeError если все заняты# - release(conn: DatabaseConnection) — возвращает соединение в пул# - property active_count -> int — количество соединений в использовании# - Должен быть потокобезопасным (использовать threading.Lock)# Ожидаемый клиентский код:# pool = ConnectionPool(max_size=3)# pool2 = ConnectionPool(max_size=10) # тот же объект, max_size не меняется# assert pool is pool2## conn1 = pool.acquire()# conn2 = pool.acquire()# result = conn1.execute("SELECT * FROM users")# print(result)# pool.release(conn1)# print(f"Активных соединений: {pool.active_count}")
<?phpclassDatabaseConnection{privatestaticint$counter=0;publicreadonlyint$id;publicbool$inUse=false;publicfunction__construct(){$this->id=++self::$counter;}publicfunctionexecute(string$query):string{return"[Conn #{$this->id}] Результат: {$query}";}publicfunction__toString():string{return"Connection #{$this->id}";}}// TODO: реализовать Одиночку ConnectionPool:// - getInstance(maxSize = 5) — всегда возвращает один объект// - acquire() -> DatabaseConnection — возвращает свободное соединение// или бросает RuntimeException если все заняты// - release(DatabaseConnection conn) — возвращает соединение в пул// - getActiveCount() -> int — количество соединений в использовании// Ожидаемый клиентский код:// $pool = ConnectionPool::getInstance(maxSize: 3);// $pool2 = ConnectionPool::getInstance(maxSize: 10); // тот же объект// assert($pool === $pool2);//// $conn1 = $pool->acquire();// $conn2 = $pool->acquire();// echo $conn1->execute("SELECT * FROM users") . "\n";// $pool->release($conn1);// echo "Активных соединений: " . $pool->getActiveCount() . "\n";
Задача 3. Счётчик событий приложения
Реализуйте ОдиночкуEventCounter, который глобально отслеживает количество различных событий в приложении. Он должен поддерживать методы increment(event), decrement(event), get(event) и reset(event). Покажите, что счётчики, изменённые в одном модуле, корректно отображаются в другом.
Реализуйте ОдиночкуMemoryCache — простой кэш с поддержкой TTL (времени жизни записи). Кэш должен предоставлять методы set(key, value, ttl), get(key) (возвращает None/null если запись истекла или отсутствует), delete(key) и flush(). При обращении к get() просроченные записи должны автоматически удаляться.
<?php// TODO: реализовать Одиночку MemoryCache с методами:// - set(key: string, value: mixed, ttl: float = 60.0): void// сохраняет значение с временем жизни в секундах// - get(key: string): mixed// возвращает значение или null, если истекло / не существует// - delete(key: string): void// - flush(): void — очищает весь кэш// - getSize(): int — количество живых записей// Ожидаемый клиентский код:// $cache = MemoryCache::getInstance();// $cache2 = MemoryCache::getInstance();// assert($cache === $cache2);// $cache->set('user:1', ['name' => 'Анна', 'role' => 'admin'], ttl: 2.0);// $cache->set('user:2', ['name' => 'Иван', 'role' => 'user'], ttl: 60.0);// var_dump($cache->get('user:1')); // ['name' => 'Анна', ...]// var_dump($cache2->get('user:2')); // ['name' => 'Иван', ...]// sleep(3);// var_dump($cache->get('user:1')); // NULL — TTL истёк// echo $cache->getSize(); // 1 — только user:2 живой
Задача 5. Менеджер feature-флагов
Реализуйте ОдиночкуFeatureFlagManager, который управляет флагами функциональности приложения. Флаги задаются при инициализации и могут динамически включаться/выключаться во время работы. Реализуйте методы is_enabled(flag), enable(flag), disable(flag) и get_all(). Убедитесь, что состояние флагов едино во всём приложении, и добавьте защиту от обращения к несуществующим флагам.
// --- в обработчике запроса ---
// \(fm = FeatureFlagManager::getInstance(); // тот же объект
// if (\)fm->isEnabled('dark_mode')) {
// echo "Тёмная тема активна\n";
// }