Когда необходимо использовать порождающий паттерн одиночка
Перейти к содержимому

Когда необходимо использовать порождающий паттерн одиночка

Программируем просто

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

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

Одиночка определяет операцию Instance, которая позволяет клиентам получать доступ к единственному экземпляру. Клиенты имеют доступ к одиночке только через эту операцию.

Паттерн позволяет избежать засорения пространства имен глобальными переменными, в которых хранятся уникальные экземпляры. От класса Singleton можно порождать подклассы, а приложение легко сконфигурировать экземпляром расширенного класса.

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

Начиная с С++11 инициализация static переменных, является потокобезопасной (If multiple threads attempt to initialize the same static local variable concurrently, the initialization occurs exactly once (similar behavior can be obtained for arbitrary functions with std::call_once).
Note: usual implementations of this feature use variants of the double-checked locking pattern, which reduces runtime overhead for already-initialized local statics to a single non-atomic boolean comparison. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. )
Поэтому классический пример одиночки Мэйерса(представленный ниже), является потокобезопасным.

#include struct logSingleton < void init()  // вызывается гарантированно один раз // выполняем открытие файла // выполняем нужные действия std::cout static logSingleton& instance() < static logSingleton res; return res; >void log() < std::cout private: logSingleton() < init(); // инициализация выполняется один раз > logSingleton(const logSingleton&) = delete; logSingleton & operator=(const logSingleton&) = delete; logSingleton(const logSingleton&&) = delete; logSingleton &operator=(const logSingleton&&) = delete; >; int main() < < logSingleton& s1 = logSingleton::instance(); s1.log(); > < logSingleton& s2 = logSingleton::instance(); s2.log(); > return 0; > 

Какие есть проблемы у одиночек.
Проблема с созданием объекта, довольно сложно сделать удобный механиз, с помощью которого мы имели бы возможность вызывать метод instance(), с различными аргументами, по факту нужно описать несколько конструкторов и столько же функций instance().
Объект одиночка, живет до конца жизни программы, лично для меня это не является проблемой, но некоторые считают что это проблема. Опять таки, все зависит от задач, и всегда можно сделать реализацию одиночки так, чтобы можно было освобождаться не от одиночки, а от данных которые он держит в памяти. Проблема с наследованием, я советую вообще не пытаться наследовать одиночку, потому что с наследованием связано много подводных камней, которые трудно вылавливать, если вам нужно наследоваться от одиночки, то скорее всего вам не нужен одиночка, а нужно что-то другое, и возможно это будет одно решение для конкретной задачи, и паттерном называться уже не будет. Наследуюясь вы должны обратить внимание на статический метод instance() базового класса, оставлять таким как есть? или менять?

#include struct SingBase  static SingBase &instance() protected: SingBase() < std::cout ~SingBase() < std::cout SingBase()" >; struct SingType1 : SingBase  static SingType1 &instance() protected: SingType1() < std::cout << "SingType1()" ~SingType1() < std::cout SingType1()" >; int main() < SingBase::instance(); SingType1::instance(); SingBase::instance(); SingType1::instance(); return 0; > 

Сложно проследить зависимость одних одиночек от других.
Всегда думайте и размышляйте 🙂
Можно завести специальное место, в начале функции main() в котором вы вызовете все методы instance(), всех одиночек, этим вы уже оградите себя от ряда проблем.
Но если следовать правилу — код должен быть простым, код пишут для человека, а компилятор переводит этот код для компьютера, то только это одно правило уже избавляет от многих проблем.

Порождающие паттерны проектирования: для каких задач нужны, виды и примеры реализации

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

Еще раз про паттерны

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

Всего существует 23 классических паттерна, которые были описаны в книге «Банды четырех». В зависимости от того, какие задачи они решают, делятся на порождающие, структурные и поведенческие.

Порождающие паттерны

Согласно Википедии, порождающие шаблоны (creational patterns) — шаблоны проектирования, которые позволяют сделать систему независимой от способа создания, композиции и представления объектов.

Проще говоря, порождающие паттерны предназначены для создания экземпляра объекта или группы связанных объектов. К ним относятся:

  • Singleton, или Одиночка
  • Builder, или Строитель
  • Factory Method, или Фабричный метод
  • Prototype, или Прототип
  • Abstract Factory, или Абстрактная фабрика

3 самых популярных порождающих паттерна

По мнению разработчиков MediaSoft Singleton, Builder и Factory Method — это самые используемые порождающие паттерны в разработке. Давайте разберемся, с какими задачами они помогают справляться, и посмотрим на примеры их реализации.

Singleton (Одиночка)

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

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

Еще проще: хороший пример этого паттерна из жизни — это классный журнал в школе. У каждого класса он только один, и если учитель спросит журнал, то всегда получит один и тот же экземпляр.

Когда нужен: пригодится, если в приложении есть управляющий объект, в котором хранится весь контекст приложения. Например, в сервисах хранения данных.

Как создать:

  1. Создаем класс, в котором прописываем логику.
  2. Создаем статическое поле и инициализируем его.
  3. Дальше создаем приватный инициализатор. Это гарантирует, что никакой другой клиент или класс не сможет создавать экземпляр этого класса.
  4. Теперь, чтобы использовать методы нашего класса, мы получаем доступ через статическое поле.

Пример реализации:

public class Singleton < private static Singleton instance; private Singleton () <>; public static synchronized Singleton getInstance() < if (instance == null) < instance = new Singleton(); >return instance; > >
// создаем класс Singleton, который проверяет имеется ли у него свойство instance // если нет, тогда создается новый экземпляр, но если свойство есть // тогда мы возвращаем ранее созданный экземпляр класса class Singleton < constructor(data) < if (Singleton.instance) < return Singleton.instance; >Singleton.instance = this; this.data = data; > consoleData() < console.log(this.data); >>; // в работе Singleton мы можем убедиться создав 2 экземпляра класса const firstSingleton = new Singleton('firstSingleton'); const secondSingleton = new Singleton('secondSingleton'); // в обоих случаях в консоли выведется одинаковое сообщение firstSingleton.consoleData(); //firstSingleton secondSingleton.consoleData(); //firstSingleton 

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

Builder (Строитель)

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

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

Еще проще: пример этого паттерна в жизни — покупка компьютера в магазине. При выборе мы указываем, какими характеристиками должна обладать техника (например, память 16 ГБ, процессор Intel core i7 и так далее). Таким образом, мы можем создавать различные виды одного объекта.

Когда нужен: пригодится при составлении SQL-запроса, а также в юнит-тестах.

Как создать:

  1. Создаем абстрактный класс Строитель, у которого объявляем методы инициализации параметров продукта.
  2. Создаем классы, наследующие Строитель, и переопределяющие методы инициализации параметров продукта.
  3. Создаем класс-распорядитель, который выполняет скоординированные действия по созданию Продукта при помощи Строителя

Пример реализации:

class Pizza < private String dough = ""; private String sauce = ""; public void setDough(String dough) < this.dough = dough; >public void setSauce(String sauce) < this.sauce = sauce; >> abstract class PizzaBuilder < // Abstract Builder protected Pizza pizza; public void createNewPizzaProduct() < pizza = new Pizza(); >public abstract void buildDough(); public abstract void buildSauce(); > class SpicyPizzaBuilder extends PizzaBuilder < // Concrete Builder public void buildDough() < pizza.setDough("pan baked"); >public void buildSauce() < pizza.setSauce("hot"); >> class Waiter < // Director private final PizzaBuilder pizzaBuilder; public Waiter(PizzaBuilder pb) < pizzaBuilder = pb; >public void constructPizza() < pizzaBuilder.createNewPizzaProduct(); pizzaBuilder.buildDough(); pizzaBuilder.buildSauce(); >> public class BuilderExample < // A customer ordering a pizza public static void main(String[] args) < Waiter waiter = new Waiter(new SpicyPizzaBuilder()); waiter.constructPizza(); >>
class Apartment < constructor(options) < for (const option in options) < this[option] = options[option]; >> getOptions() < return Количество комнат: $, площадь: $, этаж: $; > > class ApartmentBuilder < setRoomsNumber(roomsNumber) < this.roomsNumber = roomsNumber; return this; >setFloor(floor) < this.floor = floor; return this; >setSquare(square) < this.square = square; return this; >build() < return new Apartment(this); >> const bigApartment = new ApartmentBuilder() .setFloor(10) .setRoomsNumber(5) .setSquare(120) .build(); console.log(bigApartment.getOptions());

Комментарий: Ключевая идея паттерна в том, чтобы вынести сложную логику создания объекта в отдельный класс «Строитель». Этот класс позволяет создавать сложные объекты поэтапно, на выходе получая готовый объект с нужными нам характеристиками и предоставляя классу упрощенный интерфейс для работы.

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

Factory Method (Фабричный метод)

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

Проще говоря, паттерн позволяет использовать один класс для создания объектов разной реализации интерфейса и делегировать логику дочерним классам.

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

Когда нужен: паттерн подходит для ситуаций, когда нам необходимо выполнить действие (например, отправить запрос), но заранее неизвестны все данные, так как они зависят от входных параметров (например, от протокола передачи данных — rest, soap или socket).

Как создать:

  1. Создаем класс TicketFactory.
  2. Добавляем методы, которые принимают нужные параметры, и создают сущности, которые инициализируются переданными параметрами.
  3. Внутри этих методов вызываются конструкторы и сеттеры для инициализации параметров.
  4. На выходе нам возвращается инициализированная сущность.

Пример реализации:

interface Product < >class ConcreteProductA implements Product < >class ConcreteProductB implements Product < >abstract class Creator < public abstract Product factoryMethod(); >class ConcreteCreatorA extends Creator < public Product factoryMethod() < return new ConcreteProductA(); >> class ConcreteCreatorB extends Creator < public Product factoryMethod() < return new ConcreteProductB(); >> public class FactoryMethodExample < public static void main(String[] args) < Listcreators = List.of(new ConcreteCreatorA(), new ConcreteCreatorB()); creators.stream().map(Creator::factoryMethod).map(Object::getClass).forEach(System.out::println); > >
class Apartment < constructor(roomsNumber, square, floor) < this.roomsNumber = roomsNumber; this.floor = floor; this.square = square; >getOptions() < return Количество комнат: $, площадь: $, этаж: $; > >; class ApartmentFactory < createApartament(type, floor) < switch(type) < case('oneRoom'): return new Apartment(1, 35, floor); case('twoRooms'): return new Apartment(2, 55, floor); case('threeRooms'): return new Apartment(3, 75, floor); default: throw new Error('Такой квартиры не найдено'); >> > const oneRoomAparnament = new ApartmentFactory().createApartament('oneRoom', 10); console.log(oneRoomAparnament.getOptions());

ЗАКЛЮЧЕНИЕ

Паттерны проектирования — это решения распространенных проблем при разработке кода. Их знание и использование позволяет экономить время, используя готовые решения, стандартизировать код и повысить общий словарь.

В зависимости от того, какие задачи решают паттерны проектирования, они делятся на три вида: порождающие, структурные и поведенческие.

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

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

Builder, или Строитель, позволяет создавать разные объекты с заданным состоянием, используя один и тот же код. Будет полезен при составлении SQL-запроса или в юнит-тестах.

Factory method, или Фабричный метод, позволяет использовать один класс для создания объектов разной реализации интерфейса и делегировать логику дочерним классам. Подходит для ситуаций, когда нам необходимо выполнить действие, но способ выполнения неизвестен заранее.

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

Паттерн Singleton (одиночка,синглет)

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

Паттерн Singleton предоставляет такие возможности.

Описание паттерна Singleton

Архитектура паттерна Singleton основана на идее использования глобальной переменной, имеющей следующие важные свойства:

  1. Такая переменная доступна всегда. Время жизни глобальной переменной — от запуска программы до ее завершения.
  2. Предоставляет глобальный доступ, то есть, такая переменная может быть доступна из любой части программы.

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

Для решения этой проблемы паттерн Singleton возлагает контроль над созданием единственного объекта на сам класс. Доступ к этому объекту осуществляется через статическую функцию-член класса, которая возвращает указатель или ссылку на него. Этот объект будет создан только при первом обращении к методу, а все последующие вызовы просто возвращают его адрес. Для обеспечения уникальности объекта, конструкторы и оператор присваивания объявляются закрытыми.

UML-диаграмма классов паттерна Singleton

UML-диаграмма классов паттерна Singleton

Паттерн Singleton часто называют усовершенствованной глобальной переменной.

Реализация паттерна Singleton

Классическая реализация Singleton

Рассмотрим наиболее часто встречающуюся реализацию паттерна Singleton.

// Singleton.h class Singleton < private: static Singleton * p_instance; // Конструкторы и оператор присваивания недоступны клиентам Singleton() <>Singleton( const Singleton& ); Singleton& operator=( Singleton& ); public: static Singleton * getInstance() < if(!p_instance) p_instance = new Singleton(); return p_instance; >>; // Singleton.cpp #include "Singleton.h" Singleton* Singleton::p_instance = 0;

Клиенты запрашивают единственный объект класса через статическую функцию-член getInstance() , которая при первом запросе динамически выделяет память под этот объект и затем возвращает указатель на этот участок памяти. Впоследcтвии клиенты должны сами позаботиться об освобождении памяти при помощи оператора delete .

Последняя особенность является серьезным недостатком классической реализации шаблона Singleton. Так как класс сам контролирует создание единственного объекта, было бы логичным возложить на него ответственность и за разрушение объекта. Этот недостаток отсутствует в реализации Singleton, впервые предложенной Скоттом Мэйерсом.

Singleton Мэйерса

// Singleton.h class Singleton < private: Singleton() <>Singleton( const Singleton&); Singleton& operator=( Singleton& ); public: static Singleton& getInstance() < static Singleton instance; return instance; >>;

Внутри getInstance() используется статический экземпляр нужного класса. Стандарт языка программирования C++ гарантирует автоматическое уничтожение статических объектов при завершении программы. Досрочного уничтожения и не требуется, так как объекты Singleton обычно являются долгоживущими объектами. Статическая функция-член getInstance() возвращает не указатель, а ссылку на этот объект, тем самым, затрудняя возможность ошибочного освобождения памяти клиентами.

Приведенная реализация паттерна Singleton использует так называемую отложенную инициализацию (lazy initialization) объекта, когда объект класса инициализируется не при старте программы, а при первом вызове getInstance() . В данном случае это обеспечивается тем, что статическая переменная instance объявлена внутри функции — члена класса getInstance() , а не как статический член данных этого класса. Отложенную инициализацию, в первую очередь, имеет смысл использовать в тех случаях, когда инициализация объекта представляет собой дорогостоящую операцию и не всегда используется.

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

Улучшенная версия классической реализации Singleton

С учетом всего вышесказанного классическая реализация паттерна Singleton может быть улучшена.

// Singleton.h class Singleton; // опережающее объявление class SingletonDestroyer < private: Singleton* p_instance; public: ~SingletonDestroyer(); void initialize( Singleton* p ); >; class Singleton < private: static Singleton* p_instance; static SingletonDestroyer destroyer; protected: Singleton() < >Singleton( const Singleton& ); Singleton& operator=( Singleton& ); ~Singleton() < >friend class SingletonDestroyer; public: static Singleton& getInstance(); >; // Singleton.cpp #include "Singleton.h" Singleton * Singleton::p_instance = 0; SingletonDestroyer Singleton::destroyer; SingletonDestroyer::~SingletonDestroyer() < delete p_instance; >void SingletonDestroyer::initialize( Singleton* p ) < p_instance = p; >Singleton& Singleton::getInstance() < if(!p_instance) < p_instance = new Singleton(); destroyer.initialize( p_instance); >return *p_instance; >

Ключевой особенностью этой реализации является наличие класса SingletonDestroyer , предназначенного для автоматического разрушения объекта Singleton. Класс Singleton имеет статический член SingletonDestroyer , который инициализируется при первом вызове Singleton::getInstance() создаваемым объектом Singleton . При завершении программы этот объект будет автоматически разрушен деструктором SingletonDestroyer (для этого SingletonDestroyer объявлен другом класса Singleton ).

Для предотвращения случайного удаления пользователями объекта класса Singleton , деструктор теперь уже не является общедоступным как ранее. Он объявлен защищенным.

Использование нескольких взаимозависимых одиночек

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

  • Как гарантировать, что к моменту использования одного одиночки, экземпляр другого зависимого уже создан?
  • Как обеспечить возможность безопасного использования одного одиночки другим при завершении программы? Другими словами, как гарантировать, что в момент разрушения первого одиночки в его деструкторе еще возможно использование второго зависимого одиночки (то есть второй одиночка к этому моменту еще не разрушен)?

Управлять порядком создания одиночек относительно просто. Следующий код демонстрирует один из возможных методов.

// Singleton.h class Singleton1 < private: Singleton1() < >Singleton1( const Singleton1& ); Singleton1& operator=( Singleton1& ); public: static Singleton1& getInstance() < static Singleton1 instance; return instance; >>; class Singleton2 < private: Singleton2( Singleton1& instance): s1( instance) < >Singleton2( const Singleton2& ); Singleton2& operator=( Singleton2& ); Singleton1& s1; public: static Singleton2& getInstance() < static Singleton2 instance( Singleton1::getInstance()); return instance; >>; // main.cpp #include «Singleton.h» int main()

Объект Singleton1 гарантированно инициализируется раньше объекта Singleton2 , так как в момент создания объекта Singleton2 происходит вызов Singleton1::getInstance() .

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

Несмотря на кажущуюся простоту паттерна Singleton (используется всего один класс), его реализация не является тривиальной.

Результаты применения паттерна Singleton

Достоинства паттерна Singleton

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

Недостатки паттерна Singleton

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

Когда необходимо использовать порождающий паттерн одиночка

Одиночка – паттерн, порождающий объекты.

Условия, Задача, Назначение

Гарантирует, что у класса будет не более 1 созданного экземпляра, предоставляет к нему глобальную точку доступа (обычно статический метод).

Мотивация

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

Как показывает опыт, в каждом более-менее большом приложении целесообразно иметь немало таких объектов: в системе может быть много принтеров, но возможен лишь один спулер; должны быть только одна файловая система и единственный оконный менеджер; в цифровом фильтре может находиться только один аналого-цифровой преобразователь (АЦП); бухгалтерская система обслуживает только одну компанию и т.д.

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

Более удачное решение — сам класс контролирует то, что у него есть только один экземпляр, может запретить создание дополнительных экземпляров, перехватывая запросы на создание новых объектов, и он же способен предоставить доступ к своему экземпляру. Это и есть назначение паттерна одиночка.

Признаки применения, использования паттерна Одиночка (Singleton)

  1. Должен быть ровно один экземпляр некоторого класса, легко доступный всем клиентам
  2. Единственный экземпляр должен расширяться путем порождения подклассов, и клиентам нужно иметь возможность работать с расширенным экземпляром без модификации своего кода.

Решение

Участники паттерна Одиночка (Singleton)

  1. Singleton — одиночка.
    Определяет операцию получения экземпляра (назовем ее Instance) — статический метод, который позволяет клиентам получать доступ к единственному экземпляру. Может нести ответственность за создание собственного уникального экземпляра.

Схема использования паттерна Одиночка (Singleton)

Клиенты получают доступ к экземпляру класса Singleton только через его операцию Instance. Таким образом, когда нужно создать, получить объект этого класса – клиент не инстанцирует его напрямую, а вызывает эту операцию Instance(статический метод) – и получает объект.

Вопросы, касающиеся реализации паттерна Одиночка (Singleton)

При реализации паттерна одиночка (singleton) необходимо рассмотреть следующие вопросы:

  1. Гарантирование единственного экземпляра.
    Паттерн одиночка устроен так, что тот единственный экземпляр, который имеется у класса, — самый обычный, но больше одного экземпляра создать не удастся. Чаще всего для этого прячут операцию, создающую экземпляры (защищенный или закрытый конструктор), за операцией класса (то есть за статической функцией-членом или методом класса), которая гарантирует создание не более одного экземпляра. Данная операция имеет доступ к переменной, где хранится уникальный экземпляр, и гарантирует инициализацию переменной этим экземпляром перед возвратом ее клиенту. При таком подходе можно не сомневаться, что одиночка будет создан и инициализирован перед первым использованием.
  2. Порождение подклассов Singleton. Основной вопрос не столько в том, как определить подкласс, а в том, как сделать, чтобы клиенты могли использовать только его единственный экземпляр, никак не создав других экземпляров. По существу, переменная, ссылающаяся на экземпляр одиночки, должна инициализироваться вместе с экземпляром подкласса. Простейший способ добиться этого — определить одиночку, которого нужно инстанцировать в операции Instance() класса Singleton.
    Другой способ выбора подкласса Singleton — вынести реализацию операции Instance из родительского класса и поместить ее в подкласс, т.е. разрешить переопределение этой операции. Это позволит программисту задать класс одиночки на этапе компиляции, но от клиента одиночка будет по-прежнему скрыт.
    Такой подход фиксирует выбор класса одиночки на этапе компиляции, затрудняя тем самым его подмену во время выполнения. Применение условных операторов для выбора подкласса увеличивает гибкость решения, но все равно множество возможных классов Singleton остается жестко «зашитым» в код. В общем случае ни тот, ни другой подход не обеспечивают достаточной гибкости.
    Искомой гибкости можно добиться за счет использования реестра одиночек. Вместо того чтобы задавать множество возможных классов Singleton в операции Instance(), одиночки могут регистрировать себя по имени в некотором всем известном реестре. Реестр сопоставляет одиночкам строковые имена (ключи). Когда операции Instance() нужен некоторый одиночка, она запрашивает его у реестра по имени. Начинается поиск указанного одиночки, и, если он существует, реестр возвращает его. Такой подход освобождает Instance от необходимости «знать» все возможные классы или экземпляры Singleton. Нужен лишь единый для всех классов Singleton интерфейс операций с реестром, api реестра т.е.:

RegSingleton, api реестра [java] ссылка

Операция Register регистрирует экземпляр класса RegSingleton (или его подклассов) под указанным именем. Чтобы не усложнять реестр, мы будем хранить в нем список объектов в виде пар «ключ-значение» (хеш-таблица). Каждый ключ (имя) отображается на один объект «одиночка». Операция Lookup ищет одиночку по ключу (имени), например – из данных property-файла.
Но в какой момент подклассы RegSingleton (SingletonSub например) будут регистрировать себя? Одна из возможностей – конструктор, но, разумеется, конструктор не будет вызван, пока кто-то не инстанцирует класс, но ведь это та самая проблема, которую паттерн одиночка и пытается разрешить! Существуют некоторые обходные пути: в Java, например, это можно решить, задействовав статический блок инициализации, который просто лишь вызовет конструктор, создавая объект и ни к чему его не привязывая (метод Register его затем сам привяжет к регистру!):

SingletonSub, через стат-инициализатор [java] ссылка

Результаты

Достоинства паттерна одиночка (Singleton):

  1. Контролируемый доступ к единственному экземпляру.
    Поскольку класс Singleton инкапсулирует свой единственный экземпляр, он полностью контролирует то, как и когда клиенты получают доступ к нему
  2. Уменьшение числа имен.
    Паттерн одиночка — шаг вперед по сравнению с глобальными переменными. Он позволяет избежать засорения пространства имен глобальными переменными, в которых хранятся уникальные экземпляры.
  3. Допускает уточнение операций и представления.
    От класса Singleton можно порождать подклассы, а приложение легко сконфигурировать экземпляром расширенного класса. Можно параметризировать приложение экземпляром того класса, который необходим во время выполнения.
  4. Допускает переменное число экземпляров.
    Очень важное достигнутое преимущество. Паттерн позволяет вам легко изменить свое решение и разрешить появление более одного экземпляра класса Singleton. Вы можете применять один и тот же подход для управления числом экземпляров, используемых в приложении. Изменить нужно будет лишь операцию, дающую доступ к экземпляру класса Singleton. Фактически, дается возможность контролировать количество инстанцированных экземпляров класса, фигурирующих где бы то ни было в приложении.
  5. Большая гибкость, чем у операций класса.
    Еще один способ реализовать функциональность одиночки — использовать операции класса, то есть статические члены. Но это препятствует изменению дизайна, если потребуется разрешить наличие нескольких экземпляров класса. Кроме того, статические члены не полиморфны, так что их нельзя будет переопределять в подклассах.

Пример

Предположим, что у нас есть имеется некоторая абстрактная фабрика SomeFactory. Абстрагируемся от продуктов фабрики, от логики ее работы и т.д. – здесь это совершенно не принципиально.

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

Нужно также сделать конструктор защищенным, чтобы предотвратить случайное инстанцирование, в результате которого будет создан лишний экземпляр: SomeFactory.

Теперь рассмотрим случай, что у нашей фабрики существуют подклассы: WiseFactory и QuickFactory. Конкретный подкласс фабрики, использующийся в приложении – определяется системными настройками. Все что нам нужно сделать – это добавить в операцию instance() все того же класса SomeFactory код выбора конкретной фабрики в зависимости от значения этого системного свойства – и конечно обеспечить подклассы фабрики защищенным конструктором (не то появится возможность напрямую инстанцировать фабрики, минуя все наши запреты): SomeFactory.

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

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

Известные применения паттерна Одиночка (Singleton)

Примером паттерна одиночка в Smalltalk-80 является множество изменений кода, представленное классом ChangeSet. Более тонкий пример – это отношение между классами и их метаклассами. Метаклассом называется класс класса, каждый метакласс существует в единственном экземпляре. У метакласса нет имени (разве что косвенное, определяемое экземпляром), но он контролирует свой уникальный экземпляр, и создать второй обычно не разрешается.

В библиотеке Interviews для создания пользовательских интерфейсов — паттерн одиночка применяется для доступа к единственным экземплярам классов Session (сессия) и WidgetKit (набор виджетов). Классом Session определяется главный цикл распределения событий в приложении. Он хранит пользовательские настройки стиля и управляет подключением к одному или нескольким физическим дисплеям. WidgetKit — это абстрактная фабрика для определения внешнего облика интерфейсных виджетов. Операция WidgetKit: instance () определяет конкретный инстанцируемый подкласс WidgetKit на основе переменной среды, которую устанавливает Session. Аналогичная операция в классе Session «выясняет», поддерживаются ли монохромные или цветные дисплеи, и соответственно конфигурирует одиночку Session.

Родственные паттерны

Паттерн одиночка (Singleton) целесообразно применять вместе со многими паттернами, например, такими так: абстрактная фабрика, строитель, прототип.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *