Правда в том, что синглтоны и наследование не очень хорошо сочетаются друг с другом.
Да, да, любители синглтона и культ GoF будут повсюду за меня, говоря: «Хорошо, если вы сделаете свой конструктор защищенным... "и" у вас нет метода getInstance
для класса, вы можете поместить его ... ", но они просто доказывают мою точку зрения.Синглтоны должны перепрыгнуть через несколько обручей, чтобы быть и одиночным, и базовым классом.
Но просто чтобы ответить на вопрос, скажем, у нас есть базовый класс синглтона.Он может даже в какой-то степени усилить свою единственность посредством наследования.(Конструктор делает одну из немногих вещей, которые могут работать, когда он больше не может быть закрытым: он выдает исключение, если другой Base
уже существует.) Скажем, у нас также есть класс Derived
, который наследуется от Base
.Поскольку мы разрешаем наследование, давайте также скажем, что может быть любое количество других подклассов Base
, которые могут или не могут наследоваться от Derived
.
Но есть проблема - та, которую вылибо уже сталкиваешься, либо скоро.Если мы вызовем Base::getInstance
без создания объекта, мы получим нулевой указатель.Мы бы хотели вернуть любой существующий объект-одиночка (это может быть Base
, и / или Derived
, и / или Other
).Но это трудно сделать и все же следовать всем правилам, потому что есть только несколько способов сделать это - и у всех них есть некоторые недостатки.
Мы могли бы просто создатьBase
и верните его.Винт Derived
и Other
.Конечный результат: Base::getInstance()
всегда возвращает ровно Base
.Детские классы никогда не играют.Кинда побеждает цель, ИМО.
Мы могли бы поставить getInstance
нашего собственного в нашем производном классе, и попросить вызывающего абонента сказать Derived::getInstance()
, если они специально хотят Derived
,Это значительно увеличивает сцепление (потому что вызывающий теперь должен знать, что конкретно запрашивает Derived
, и заканчивает тем, что привязывает себя к этой реализации).
Мы могли бы сделать вариант этого последнегоодин - но вместо того, чтобы получить экземпляр, функция просто создает один.(Пока мы находимся, давайте переименуем функцию в initInstance
, так как нам не особо важно, что она получит - мы просто вызываем ее, чтобы она создала новый Derived
и установила его как единое целое.True Instance.)
Итак, (за исключением каких-либо странностей, еще не учтенных), это работает примерно так ...
class Base {
static Base * theOneTrueInstance;
public:
static Base & getInstance() {
if (!theOneTrueInstance) initInstance();
return *theOneTrueInstance;
}
static void initInstance() { new Base; }
protected:
Base() {
if (theOneTrueInstance) throw std::logic_error("Instance already exists");
theOneTrueInstance = this;
}
virtual ~Base() { } // so random strangers can't delete me
};
Base* Base::theOneTrueInstance = 0;
class Derived : public Base {
public:
static void initInstance() {
new Derived; // Derived() calls Base(), which sets this as "the instance"
}
protected:
Derived() { } // so we can't be instantiated by outsiders
~Derived() { } // so random strangers can't delete me
};
И в вашем коде инициализации,Вы говорите Base::initInstance();
или Derived::initInstance();
, в зависимости от того, какой тип вы хотите, чтобы синглтон был.Вам, конечно, придется приводить возвращаемое значение из Base::getInstance()
, чтобы использовать любые Derived
-специфичные функции, но без приведения вы можете использовать любые функции, определенные в Base
, включая виртуальные функции, переопределенные Derived
.
Обратите внимание, что у этого способа также есть ряд собственных недостатков, однако:
Он возлагает большую часть бремени обеспечения единства на базуучебный класс.Если база не обладает подобной или подобной функциональностью, и вы не можете ее изменить, вы как бы облажались.
Базовый класс не может взять все ответственности, хотя - каждый класс должен объявить защищенный деструктор, или кто-то может прийти и удалить один экземпляр после приведения его (в) соответствующим образом, и все это пойдет в ад.Что еще хуже, это не может быть применено компилятором.
Поскольку мы используем защищенные деструкторы, чтобы предотвратить случайное чмо от удаления нашего экземпляра, если компилятор не умнее, чем я боюсь, даже среда выполнения не сможет правильно удалитьВаш экземпляр, когда программа заканчивается.До свидания, RAII ... привет "предупреждения об обнаружении утечки памяти".(Конечно, память в конечном итоге будет восстановлена любой приличной ОС. Но если деструктор не запустится, вы не можете полагаться на то, что он выполнит за вас очистку. Перед тем, как вывыход, и это не даст вам никаких гарантий, которые может дать RAII.)
Он предоставляет метод initInstance
, который, IMO, на самом деле не принадлежит API, который может видеть каждый.Если вы хотите, вы можете сделать initInstance
закрытым и позволить вашей функции инициализации быть friend
, но тогда ваш класс делает предположения о коде вне себя, и связующее средство возвращается в игру.
Также обратите внимание, что приведенный выше код вовсе не безопасен для потоков.Если вам это нужно, вы сами по себе.
Серьезно, менее болезненный путь - это забыть о попытках навязать единство.Наименее сложный способ гарантировать, что существует только один экземпляр, - это создать один.Если вам нужно использовать его в нескольких местах, рассмотрите внедрение зависимостей.(Не-каркасная версия этого означает «передать объект вещам, которые в нем нуждаются».: P) Я пошел и спроектировал вышеперечисленные вещи просто для того, чтобы попытаться доказать, что я не прав в отношении одиночных кодов и наследования, и просто подтвердил себе, как злокомбинация есть.Я бы не рекомендовал делать это в реальном коде.