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

Федоров Дмитрий

 

Проблемы, связанные с глобальными объектами

При разработке больших проектов, часто возникает необходимость обращаться из одного модуля программы к объектам, существующим в другом модуле. Такие объекты, как правило, существуют в единичных экземплярах, поэтому наиболее распространенной практикой является создание глобальных объектов данного типа и ссылка на них из других модулей программы с применением ключевого слова extern. Так, например, при создании ATL проекта в среде MSVC++, мастер проекта создает экземпляр класса – наследника от CComModule, _Module, в главном файле проекта, и помещает объявление extern CMyModule _Module в stdafx.h, что делает доступным объект _Module из других файлов проекта. Однако при таком подходе отсутствует механизм, предотвращающий создание нескольких объектов данного типа. Кроме того, поскольку объект создается статически, отсутствует возможность управлять процессом его создания. То есть, объект создается автоматически, до момента его фактического применения в программе. Это может приводить к некоторым неприятным последствиям: если объект работает с некоторой инфраструктурой, то инициализация и освобождение этой инфраструктуры должны быть помещены, например, в этот же класс.

листинг 1

class BusinesLogic //использует инфраструктуру COM

{

public:

 BusinesLogic () {

  CoInitializeEx(NULL, COINIT_MULTITHREADED);

  //некая работа с COM

 }

 ~BusinesLogic () {

  CoUninitialize();

 }

};

Глядя на следующий фрагмент, нельзя сказать, была ли инициализирована инфраструктура, а если да, то ничего нельзя сказать о выбранной потоковой модели COM. Как следствие, такой код – источник ошибок и возможных проблем с их диагностикой.

листинг 2

BusinesLogic BL;

void main() {

 HRESULT hr;

 IUnknown *p;

 hr=CoCreateInstance(CLSID_AppartmentThreadClass, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&p);

}

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

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

Листинг 3

class Singleton {

 static Singleton* _self;

protected:

 Singleton(){}

public:

 static Singleton* Instance() {

  if (!_self) _self = new Singleton();

  return _self;

 }

 //методы

 void aFunc1();

 void aFunc2();

 //данные

 int aData;

};

Singleton* Singleton::_self=NULL;

ПРИМЕЧАНИЕ Конструктор класса объявлен в защищенной секции. Благодаря этому отсутствует возможность создавать объекты класса по оператору new или статически. Вместо этого для конструирования объекта служит метод Instance(), который гарантирует, что в программе будет существовать только один экземпляр данного класса.

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

Модуль MAIN

#include "app.h"

void main() {

 Application* application = Application::Instance();

 application->Run();

 delete application;

}

Модуль APP

#include

using std::string;

class Window;

class Application {

 static Application* _self;

 Window *wnd;

protected:

 Application(){}

public:

 static Application* Instance();

 int loadIniInt(string& section, string& var);

 void saveIniInt(string& section, string& var, int val);

 void Run();

};

Application* Application::Instance() {

 if(!_self) _self = new Application();

 return _self;

}

int Application::loadIniInt(string& section, string& var) {

 printf("loadIni\n");

 return 100;

}

 void Application::saveIniInt(string& section, string& var, int val) {

 printf("saveIni\n");

}

void Application::Run() {

 wnd=new Window();

 //цикл обработки сообщений

 delete wnd;

}

Application* Application::_self=NULL;

Модуль WINDOW

#include "app.h"

class Window {

 int width;

 int height;

public:

 Window() {

  Application *p=Application::Instance();

  p->loadIniInt(string("Window"), string("width"));

  p->loadIniInt(string("Window"), string("height"));

 }

 ~Window() {

  Application *p=Application::Instance();

  p->saveIniInt(string("Window"), string("width"), width);

  p->saveIniInt(string("Window"),string("height"), height);

 }

};

Этот листинг показывает, как можно организовать каркас оконного приложения, используя паттерн Singleton. Из класса окна требуется доступ к некоторым функциям объекта Application. Поскольку объект приложения существует всегда в одном экземпляре, то он реализует паттерн Singleton, а доступ к объекту приложения из объекта окна осуществляется благодаря методу Instance().

 

Проблема удаления объекта “Singleton”.

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

Листинг 5

class Client {

 Singleton * _pS;

public:

 SetObject(Singleton *p) {_pS=p;}

 ~Client(){delete _pS;}

};

void main() {

 Client c1,c2;

 c1.SetObject(Singleton::Instance());

 c2.SetObject(Singleton::Instance());

}

Эта программа будет пытаться удалить дважды один и тот же объект, что приведет к исключительной ситуации в программе. При выходе из контекста функции main, сначала будет вызван деструктор объекта c2, который удалит объект класса Singleton, а затем то же самое попытается сделать и деструктор объекта c1. В связи с этим, хотелось бы иметь механизм, позволяющий автоматически отслеживать ссылки на объект класса Singleton, и автоматически удалять его только тогда, когда на объект нет активных ссылок. Для этого используют специальный метод FreeInst(), удаляющий объект только в случае, если активных ссылок на него нет.

Другая задача, которую надо решить – запрет удаления клиентом объекта Singleton посредством оператора delete. Это решается помещением деструктора в секцию protected.Тем самым, клиенту ничего не остается, как использовать пару Instance()/FreeInst() для управления временем жизни объекта.

Листинг 6

class Singleton {

protected:

 static Singleton* _self;

 static int _refcount;

 Singleton(){};

 ~Singleton(){};

public:

 static Singleton* Instance();

  void FreeInst() {_refcount--; if(!_refcount) {delete this; _self=NULL;}}

};

В данном примере, в класс Singleton введен счетчик ссылок. Метод FreeInst() вызывает оператор удаления только тогда, когда _refcount равен нулю.

 

Проблема наследования

Если существует необходимость наследовать от класса Singleton, то следует придерживаться определенных правил.

Во-первых, класс-наследник должен переопределить метод Instance(), так, чтобы создавать экземпляр производного класса. Если не предполагается, что указатель будет использоваться полиморфно, то можно объявить возвращаемый тип метода Instance() как указатель на класс-наследник, в противном случае, метод Instance() должен возвращать указатель на базовый класс (Singleton).

Во-вторых, в базовом классе деструктор должен быть объявлен как виртуальный: в определенный момент клиент вызывает метод FreeInst для указателя на базовый класс. Поскольку метод FreeInst сводится к оператору delete this, то в случае, если деструктор не виртуальный, будет вызван деструктор базового класса, но не будет вызван деструктор класса-потомка. Чтобы избежать такой ситуации, следует явно объявить деструктор базового класса виртуальным.

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

Листинг 7

class Singleton {

protected:

 static Singleton* _self;

 static int _refcount;

 Singleton(){}

  virtual ~Singleton() {printf ("~Singleton\n");}

public:

 static Singleton* Instance();

 void FreeInst();

};

class SinglImpl: public Singleton {

protected:

  SinglImpl(){}

//объявление виртуальным в базовом классе автоматически

 //дает виртуальность в производном.

  ~SinglImpl() {printf ("~SinglImpl\n");}

public:

 static Singleton* Instance() {

  if(!_self) _self = new SinglImpl ();

  _refcount++;

  return _self;

 }

};

void main() {

 Singleton *p = SinglImpl::Instance();

 …

 …

 …

 p->FreeInst();

}

Результат работы:

~SinglImpl

~Singleton

Иногда может возникнуть ситуация, при которой клиент должен полиморфно работать с объектами, имеющими общий базовый класс, но некоторые из них реализуют паттерн Singleton, а некоторые нет. Проблема возникает в момент освобождения объектов, так как у простых классов нет механизма отслеживания ссылок, а у классов, реализующих Singleton, он есть. При вызове метода FreeInst() через указатель на базовый класс будет вызываться FreeInst() базового класса, не имеющего понятия о подсчете ссылок. Это приведет и к безусловному удалению объектов “Singleton” из памяти. Для предотвращения такого поведения следует объявить виртуальным метод FreeInst() в базовом классе и реализовать специфическое поведение метода для классов Singleton. Реализация FreeInst() в базовом классе предоставляет механизм удаления объектов, не являющихся Singleton’ами.

Листинг 8

class base {

protected:

 virtual ~base(){} //гарантируем удаление только через FreeInst()

public:

 virtual void Do1()=0;

  virtual void FreeInst(){delete this;}

};

class Simple: public base {

protected:

 ~Simple () {printf("Simple::~Simple\n");}

public:

 void Do1(){printf("Simple::Do1\n");}

};

class Singleton: public base {

 static Singleton* _self;

 static int _refcount;

protected:

 Singleton(){}

 ~Singleton () {printf("Singleton::~Singleton\n");}

public:

 static Singleton* Instance() {

  if(!_self) _self = new Singleton ();

  _refcount++;

  return _self;

 }

  void FreeInst() {_refcount--; if(!_refcount) {delete this; _self=NULL;}}

void Do1(){printf("Singleton::Do1\n");}

};

Singleton* Singleton::_self=NULL;

int Singleton:: _refcount=0;

class Client {

 base *objs[2];

 int ind;

public:

 Client(){  objs[0]=NULL;objs[1]=NULL;ind=0; }

 ~Client() {

  for(int i=0;i FreeInst ();

 }

 void Add(base *p){if(ind<2) objs[ind++]=p;}

 void Do() {

  for(int i=0;iDo1();

 }

};

void main() {

 Client cl;

 cl.Add(Singleton::Instance());

 cl.Add(new Simple());

cl.Do();

}

результат работы программы:

Singleton::Do1 Simple::Do1 Singleton::~Singleton Simple::~Simple

В данном примере при разрушении объект класса Client автоматически вызываются методы FreeInst() для каждого из хранимых указателей. Благодаря тому, что этот метод объявлен виртуальным, а в классах реализующих паттерн Singleton этот метод переопределен с учетом подсчета ссылок, то программа работает именно так как ожидается.

 

Применение шаблонов языка C++.

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

листинг 9

template

class Singleton {

 static T* _self;

 static int _refcount;

protected:

 Singleton(){}

 virtual ~Singleton(){_self=NULL;}

public:

 static T* Instance();

 void FreeInst();

};

template

T* Singleton::_self = NULL;

template

int Singleton::_refcount=0;

template

T* Singleton::Instance() {

 if(!_self) _self=new T;

 _refcount++;

 return _self;

}

template

void Singleton::FreeInst() {

 if(--_refcount==0) delete this;

}

class Derived: public Singleton {

protected:

 Derived(){}

 friend class Singleton;

};

int main(int argc, char* argv[]) {

 Derived *p = Derived::Instance();

 …

 …

 …

 p->FreeInst();

 return 0;

}

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

 

Резюме

1. В классе Singleton конструктор должен быть объявлен в защищенной секции для предотвращения создания объекта способом, отличным от вызова метода Instance().

2. Деструктор также следует поместить в защищенную секцию класса, чтобы исключить возможность удаления объекта оператором delete.

3. Для автоматического подсчета ссылок при освобождении объекта следует применять специальный метод, такой как FreeInst().

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

5. Если предполагается полиморфная работа с классами, наследуемыми от одного базового класса, причем некоторые классы потомки реализуют паттерн Singleton, а некоторые нет, следует в базовом классе определить метод FreeInst() как виртуальный. Базовый класс предоставляет реализацию по умолчанию этого метода, просто вызывая оператор delete this. В классах-потомках, реализующих паттерн Singleton, при реализации метода FreeInst(), используйте механизм подсчета ссылок.

6. Если используется параметризированная версия Singleton, то в производных классах следует объявить базовый класс (Singleton) дружественным.

 

Литература

1. http://www.firststeps.ru/theory/c/r.php?29

2. http://ooad.asf.ru/patterns/patterninfo.asp?ID=13

3. Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес “Приемы объектно-ориентированного проектирования. Паттерны проектирования”

 

Комментарии: 

Как бы так это заделать???
konst 11.12.2002 19:17

Во первых, статья очень хорошая и полезная, спасибо. Применил в реальной программе я этот сингелтон и остался очень доволен. Но мне надо было несколько изменить поведение исходного класса. У меня ситуация такая: есть клиент, который шлёт запросы на сервер, на сервере есть класс, который создаёт и хранит необходимые для обработки этих запросов таблицы. Но беда в том, что после обработки очередного запроса мне не надо уничтожать объект, т.к. эти запросы идут сериями по многу штук подряд, а удалять объект надо в конце серии... Я сделал во что:

template <class T>

T*  Singleton<T>::Instance()

{

  if(!_self)

  {

    _self=new T;

    _refcount++; // вот это

  }

  _refcount++;

  return _self;

}

Но выглядит на мой взгляд коряво. Может есть какие-то паттерны на этот случай?

Ну, в довесок хочу воткнуть так вот полу-смарт указатель:

template<class T>

class TSingeltonPtr

{

  T *_t;

public:

  TSingeltonPtr() { _t = T::Instance(); }

  ~TSingeltonPtr() { if(_t) _t->FreeInst(); }

  operator T*() { return _t; }

  T* operator->() { return _t; }

  bool operator!() const { return _t==0; }

};

использование:

class Derived : public Singleton<Derived>

{

void foo();

...

};

void bar()

{

  TSingeltonPtr<Derived> sp;

  if ( !sp ) return;

  sp->foo();

}

Маленькое замечание 
Пересыпкин Михаил Алексеевич 3.12.2002 16:39

Все таки непонятно, откуда пошла дурацкая мода в с++ коде давать данным-членам класса символ подчеркивания в качестве префикса? То есть, и так ясно откуда...

Но ведь c++ не java... Например на MCVC++ код с такими префиксами не компилируется иногда. Ведь сказано же, зарезервировано под надобности компиляторов. Или это специально так отформатировано, чтобы читать было красивее? ;)

А статья действительно хорошая, удивило то что почти не пересекается с [3] ;)

зачем возвращать указатель?
Sergeem 19.11.2002 12:56

Еще хочу добавить: почему Instance() возвращает указатель а не ссылку? Тогда и путаницы с delete не будет!

взаимодействие синглтонов 
Sergeem 19.11.2002 12:52

Интересно обсудить эту тему.

Например у меня есть синглтон - менеджер памяти, синглтон - БД-клиент, и т.д.

Тогда менеджер памяти должен "уходить" последним, поскольку другие с-тоны могут освобождать память в своих деструкторах. Интересный вариант есть в loki. Я реалиовал свою модель. феслт кому интересно, могу поделиться или обсудить где-нить в С++ форуме.