Ответ 1
Сводная версия:
Вы знаете, как часто вы используете глобальные переменные? Хорошо, теперь используйте Singletons EVEN LESS. На самом деле гораздо меньше. Больше никогда. Они разделяют все проблемы, с которыми сталкиваются глобальные группы со скрытой связью (напрямую влияющие на проверяемость и ремонтопригодность), и часто ограничение "только одно может существовать" на самом деле является ошибочным предположением.
Подробный ответ:
Самое главное, что нужно понять об одном сингле, - это глобальное состояние. Это шаблон для отображения одного экземпляра глобального неограниченного доступа. У этого есть все проблемы в программировании, которые имеют глобальные группы, но также принимает некоторые интересные новые детали реализации и в противном случае очень мало реальной ценности (или, действительно, это может приходить за излишнюю дополнительную стоимость с одним аспектом экземпляра). Реализация различна настолько, что люди часто ошибочно принимают ее за объектно-ориентированный метод инкапсуляции, когда он действительно просто причудливый единичный экземпляр глобального.
Единственная ситуация, в которой вы должны рассмотреть одноэлемент, заключается в том, что наличие более одного экземпляра уже глобальных данных будет фактически логической или аппаратной ошибкой доступа. Даже тогда вы, как правило, не должны напрямую обращаться к синглтону, но вместо этого предоставляете интерфейс оболочки, для которого разрешено создавать экземпляры столько раз, сколько вам нужно, а только для доступа к глобальному состоянию. Таким образом, вы можете продолжать использовать вложение зависимостей, и если вы можете когда-либо забыть глобальное состояние из поведения класса это не радикальное изменение в вашей системе.
Есть тонкие проблемы с этим, однако, когда кажется, что вы не полагаетесь на глобальные данные, но вы. Так что (используя вложение зависимостей интерфейса, который обертывает синглтон), является лишь предложением, а не правилом. В общем, это все же лучше, потому что по крайней мере вы можете видеть, что класс полагается на singleton, тогда как просто использование функции:: instance() внутри живота функции члена класса скрывает эту зависимость. Он также позволяет извлекать классы, основанные на глобальном состоянии, и делать для них более эффективные модульные тесты, и вы можете пройти в mock-do-nothing объектах, где, если вы испепеляете зависимость от синглтона непосредственно в классе, это намного сложнее.
При выпечке вызова singleton :: instance, который также запускается в класс, вы делаете наследование невозможным. Рабочие процессы обычно ломают "одиночный экземпляр" части синглтона. Рассмотрим ситуацию, когда у вас есть несколько проектов, основанных на общем коде в классе NetworkManager. Даже если вы хотите, чтобы этот NetworkManager был глобальным состоянием и единственным экземпляром, вы должны быть очень скептически настроены на то, чтобы превратить его в одноэлементный. Создавая простой синглтон, который создает экземпляр, вы в основном делаете невозможным, чтобы какой-либо другой проект извлекался из этого класса.
Многие считают, что ServiceLocator является анти-шаблоном, однако я считаю, что он на полшага лучше, чем Singleton, и эффективно затмевает цель шаблона Go4. Существует много способов реализовать локатор сервисов, но основная идея заключается в том, что вы разбиваете конструкцию объекта и доступ объекта к двум этапам. Таким образом, во время выполнения вы можете подключить соответствующий производный сервис, а затем получить доступ к нему из одной глобальной точки соприкосновения. Это имеет преимущество с явным построением объекта, а также позволяет получить от вашего базового объекта. Это по-прежнему плохо для большинства заявленных причин, но это меньше плохо, чем Singleton, и является заменой замены.
Один конкретный пример приемлемого одноэлементного (read: servicelocator) может заключаться в обходе интерфейса стиля c одного экземпляра, такого как SDL_mixer. Один пример сингла, часто наивно реализованный там, где он, вероятно, не должен быть, находится в классе регистрации (что происходит, когда вы хотите подключиться к консоли AND на диск? Или если вы хотите записывать подсистемы отдельно.)
Однако наиболее важные проблемы, связанные с глобальным состоянием, всегда возникают, когда вы пытаетесь внедрить надлежащее модульное тестирование(и вы должны пытаться это сделать). С вашим приложением гораздо труднее разобраться, когда в недрах классов, на которых у вас действительно нет доступа, вы пытаетесь делать чистую запись и чтение на диске, подключаться к живым серверам и отправлять реальные данные, или звучать из ваших колонок волей-неволей. Это намного, МНОГО, лучше использовать инъекцию зависимостей, чтобы вы могли макетировать класс do-nothing (и видеть, что вам нужно сделать это в конструкторе класса) в случае плана тестирования и указать на него без необходимости божественного глобальное состояние, от которого зависит ваш класс.
Ссылки по теме:
- Блестящая способность, вызываемая глобальным государством и синглтонами
- Инъекция зависимостей, чтобы избежать синглтонов
- Заводы и синглтоны
Использование шаблона vs Emergence
Шаблоны полезны в качестве идей и терминов, но, к сожалению, люди, похоже, чувствуют необходимость "использовать" шаблон, когда действительно модели реализуются по мере необходимости. Часто одноэлемент специально обучается в простоте, потому что он часто обсуждается. Создайте свою систему с осознанием шаблонов, но не создавайте свою систему специально, чтобы сгибать их только потому, что они существуют. Они являются полезными концептуальными инструментами, но так же, как вы не используете каждый инструмент в панели инструментов только потому, что можете, вы не должны делать то же самое с шаблонами. Используйте их по мере необходимости и не более или менее.
Пример одиночного экземпляра Service Locator
#include <iostream>
#include <assert.h>
class Service {
public:
static Service* Instance(){
return _instance;
}
static Service* Connect(){
assert(_instance == nullptr);
_instance = new Service();
}
virtual ~Service(){}
int GetData() const{
return i;
}
protected:
Service(){}
static Service* _instance;
int i = 0;
};
class ServiceDerived : public Service {
public:
static ServiceDerived* Instance(){
return dynamic_cast<ServiceDerived*>(_instance);
}
static ServiceDerived* Connect(){
assert(_instance == nullptr);
_instance = new ServiceDerived();
}
protected:
ServiceDerived(){i = 10;}
};
Service* Service::_instance = nullptr;
int main() {
//Swap which is Connected to test it out.
Service::Connect();
//ServiceDerived::Connect();
std::cout << Service::Instance()->GetData() << "\n" << ((ServiceDerived::Instance())? ServiceDerived::Instance()->GetData() :-1);
return 0;
}