Паттерн Одиночка (Singleton) —уровень объекта

Название и классификация паттерна

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

Назначение

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

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

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

Применимость

Используйте паттерн Одиночка, когда:

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

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

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

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

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

Для решения этой проблемы паттерн Singleton возлагает контроль над созданием единственного объекта на сам класс. Доступ к этому

Singleton

  • -instance: Singleton
  • -Singleton() +qellstance(): Singleton

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

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

Участники

Singleton — Одиночка:

  • • определяет операцию Instance, которая позволяет клиентам получать доступ к единственному экземпляру. Instance — это метод класса и статическая функция-член в C++;
  • • может нести ответственность за создание собственного уникального экземпляра.

Отношения

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

Результаты

У паттерна Одиночка есть определенные достоинства:

  • контролируемый доступ к единственному экземпляру. Поскольку класс Singleton инкапсулирует свой единственный экземпляр, он полностью контролирует то, как и когда клиенты получают доступ к нему;
  • уменьшение числа имен. Паттерн Одиночка — шаг вперед по сравнению с глобальными переменными. Он позволяет избежать засорения пространства имен глобальными переменными, в которых хранятся уникальные экземпляры;
  • допускает уточнение операций и представления. От класса Singleton можно порождать подклассы, а приложение легко сконфигурировать экземпляром расширенного класса. Можно конкретизировать приложение экземпляром того класса, который необходим во время выполнения;
  • допускает переменное число экземпляров. Паттерн позволяет легко изменить свое решение и разрешить появление более одного экземпляра класса Singleton. Вы можете применять один и тот же подход для управления числом экземпляров, используемых в приложении. Изменить нужно будет лишь операцию, дающую доступ к экземпляру класса Singleton.

Реализация

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

В C++ операция класса определяется с помощью статической функции-члена Instance класса Singleton. В этом классе есть также статическая переменная-член instance, которая содержит указатель на уникальный экземпляр.

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

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

// Singleton, h

class Singleton

{

private:

static Singleton * p_instance;

// Конструкторы и оператор присваивания недоступны клиентам Singleton() {}

Singleton( const Singleton& );

Singleton& operator=( Singleton&); public:

static Singleton * getlnstance() { if(!p_instance)

p_instance = new Singleton(); return p_instance;

}

// Singleton.cpp #include "Singleton.h"

Singleton* Singleton::p_instance = 0;

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

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

Singleton Мэйерса

// Singleton.h class Singleton {

private:

Singleton() {}

Singleton( const Singleton&);

Singleton& operator=( Singleton&); public:

static Singleton& getlnstance() { static Singleton instance; return instance;

}

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

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

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

Улучшенная версия классической реализации 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& getlnstance();

// Singleton.cpp

#include "Singleton.h"

Singleton * Singleton::p_instance = 0;

Singleton Destroyer 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::getlnstance() создаваемым объектом Singleton. При завершении программы этот объект будет автоматически разрушен деструктором SingletonDestroyer (для этого SingletonDestroyer объявлен другом класса Singleton).

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

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

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

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

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

// Singleton.h class Singleton 1 {

private:

Singleton 1() {}

Singleton 1( const Singleton 1& );

Singleton 1& operator=( Singleton 1& ); public:

static Singleton 1& getlnstance() { static Singleton 1 instance; return instance;

}

};

class Singleton2

{

private:

Singleton2( Singleton 1& instance): sl( instance) {} Singleton2( const Singleton2&);

Singleton2& operator=( Singleton2& );

Singleton 1& si; public:

static Singleton2& getlnstance() { static Singleton2 instance( Singletonl::getInstance()); return instance;

}

};

// main.cpp

#include "Singleton.h"

int main()

{

Singleton2& s = Singleton2::getInstance(); return 0;

}

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

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

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

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

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

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

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

 
< Пред   СОДЕРЖАНИЕ     След >