Синглтон: как его использовать
Изменить: Из другого вопроса я предоставил ответ, который содержит ссылки на множество вопросов / ответов о синглетонах: Подробнее о синглетонах здесь:
Итак, я прочитал ветку Singletons: хороший дизайн или костыль?
И аргумент все еще неистовствует.
Я вижу Singletons как шаблон дизайна (хороший и плохой).
Проблема синглтона не в шаблоне, а в пользователях (извините всех). Все и их отец думают, что могут правильно их реализовать (а из многих интервью, которые я провел, большинство людей не могут). Кроме того, потому что все думают, что могут реализовать правильный Singleton, они используют шаблон и используют его в ситуациях, которые не подходят (замена глобальных переменных на Singletons!).
Итак, основные вопросы, на которые необходимо ответить:
- Когда вы должны использовать синглтон
- Как правильно реализовать Singleton?
Я надеюсь, что в этой статье мы сможем собрать вместе в одном месте (вместо того, чтобы гуглить и искать на нескольких сайтах) авторитетный источник того, когда (а затем и как) правильно использовать Singleton. Также уместным будет список Анти-Использований и распространенных плохих реализаций, объясняющих, почему они не работают, и для хороших реализаций их слабые стороны.
Итак, начнем:
Я подниму руку и скажу, что это то, что я использую, но, вероятно, есть проблемы.
Мне нравится "Скотт Майерс", описывающий эту тему в своих книгах "Эффективный C++"
Хорошие ситуации, чтобы использовать Singletons (не много):
- Каркасы логирования
- Бассейны для переработки ниток
/*
* C++ Singleton
* Limitation: Single Threaded Design
* See: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
* For problems associated with locking in multi threaded applications
*
* Limitation:
* If you use this Singleton (A) within a destructor of another Singleton (B)
* This Singleton (A) must be fully constructed before the constructor of (B)
* is called.
*/
class MySingleton
{
private:
// Private Constructor
MySingleton();
// Stop the compiler generating methods of copy the object
MySingleton(MySingleton const& copy); // Not Implemented
MySingleton& operator=(MySingleton const& copy); // Not Implemented
public:
static MySingleton& getInstance()
{
// The only instance
// Guaranteed to be lazy initialized
// Guaranteed that it will be destroyed correctly
static MySingleton instance;
return instance;
}
};
ХОРОШО. Давайте получим некоторую критику и другие реализации вместе.
:-)
24 ответа
Вы все не правы. Прочитайте вопрос. Ответ:
Используйте Singleton, если:
- Вы должны иметь один и только один объект типа в системе
Не используйте Singleton, если:
- Вы хотите сохранить память
- Вы хотите попробовать что-то новое
- Вы хотите показать, сколько вы знаете
- Потому что все остальные делают это (см. Грузовой культ программист в Википедии)
- В виджетах пользовательского интерфейса
- Это должен быть кеш
- В строках
- В сессиях
- Я могу идти весь день
Как создать лучший синглтон:
- Чем меньше, тем лучше. Я минималист
- Убедитесь, что это безопасно
- Убедитесь, что оно никогда не равно нулю
- Убедитесь, что он создан только один раз
- Ленивая или системная инициализация? До ваших требований
- Иногда ОС или JVM создают синглтоны для вас (например, в Java каждое определение класса является одиночным)
- Предоставить деструктор или как-то выяснить, как распорядиться ресурсами
- Использовать мало памяти
Синглтоны дают вам возможность комбинировать две плохие черты в одном классе. Это неправильно почти во всех отношениях.
Синглтон дает вам:
- Глобальный доступ к объекту и
- Гарантия того, что не может быть создано больше одного объекта этого типа
Номер один прост. Глобалы вообще плохие. Мы никогда не должны делать объекты глобально доступными, если нам это действительно не нужно.
Номер два может звучать так, как будто это имеет смысл, но давайте подумаем об этом. Когда в последний раз вы ** случайно * создавали новый объект вместо ссылки на существующий? Поскольку это тег C++, давайте использовать пример из этого языка. Вы часто случайно пишете
std::ostream os;
os << "hello world\n";
Когда вы намеревались написать
std::cout << "hello world\n";
Конечно, нет. Нам не нужна защита от этой ошибки, потому что такого рода ошибки просто не случаются. Если это так, то правильным ответом будет идти домой и спать по 12-20 часов и надеяться, что вы почувствуете себя лучше.
Если нужен только один объект, просто создайте один экземпляр. Если один объект должен быть глобально доступен, сделайте его глобальным. Но это не значит, что нельзя создавать другие экземпляры этого.
Ограничение "возможен только один экземпляр" на самом деле не защищает нас от возможных ошибок. Но это делает наш код очень сложным для рефакторинга и поддержки. Потому что довольно часто мы узнаем позже, что нам нужно более одного экземпляра. У нас есть более одной базы данных, у нас есть более одного объекта конфигурации, нам нужно несколько регистраторов. Наши модульные тесты могут захотеть создавать и воссоздавать эти объекты каждый тест, чтобы взять общий пример.
Таким образом, синглтон должен использоваться тогда и только тогда, когда нам нужны обе черты, которые он предлагает: если нам нужен глобальный доступ (что редко, потому что глобальные списки обычно не поощряются), и нам нужно не допустить, чтобы кто- либо когда-либо создавал более одного экземпляра класс (который звучит для меня как вопрос дизайна). Единственная причина, по которой я это вижу, заключается в том, что создание двух экземпляров повредило бы состояние нашего приложения - возможно, потому, что класс содержит несколько статических членов или аналогичную глупость. В этом случае очевидный ответ - исправить этот класс. Это не должно зависеть от того, чтобы быть единственным экземпляром.
Если вам нужен глобальный доступ к объекту, сделайте его глобальным, как std::cout
, Но не ограничивайте количество экземпляров, которые могут быть созданы.
Если вам абсолютно необходимо ограничить количество экземпляров класса до одного, и нет никакого способа, которым создание второго экземпляра можно было бы безопасно обработать, то примените это. Но не делайте это глобально доступным также.
Если вам нужны обе эти черты, то 1) сделайте их синглтоном и 2) дайте мне знать, для чего вам это нужно, потому что мне трудно представить такой случай.
Проблема с синглетонами не в их реализации. Дело в том, что они объединяют два разных понятия, ни одно из которых явно не желательно.
1) Singletons обеспечивают механизм глобального доступа к объекту. Хотя они могут быть несколько более поточно-ориентированными или чуть более надежными в языках без четко определенного порядка инициализации, это использование все еще является моральным эквивалентом глобальной переменной. Это глобальная переменная, одетая в какой-то неуклюжий синтаксис (скажем, foo::get_instance() вместо g_foo), но она служит точно такой же цели (единственный объект, доступный во всей программе) и имеет точно такие же недостатки.
2) Синглтоны предотвращают множественные экземпляры класса. Это редкость, IME, что такого рода функции должны быть включены в класс. Это обычно намного более контекстная вещь; многие вещи, которые рассматриваются как "один-единственный-один", на самом деле просто-таки-случаются один-единственный. IMO более подходящее решение - просто создать только один экземпляр - пока вы не поймете, что вам нужно более одного экземпляра.
Одна вещь с шаблонами: не обобщайте. У них есть все случаи, когда они полезны, и когда они терпят неудачу.
Синглтон может быть неприятным, когда вам нужно проверить код. Как правило, вы застряли с одним экземпляром класса и можете выбирать между открытием двери в конструкторе или каким-либо методом сброса состояния и так далее.
Другая проблема заключается в том, что Синглтон на самом деле является не более чем скрытой глобальной переменной. Когда у вас слишком много общего общего состояния в вашей программе, все возвращается назад, мы все это знаем.
Это может усложнить отслеживание зависимостей. Когда все зависит от вашего синглтона, его сложнее изменить, разделить на два и т. Д. Вы обычно застряли с этим. Это также препятствует гибкости. Изучите некоторые основы внедрения зависимостей, чтобы попытаться облегчить эту проблему.
Синглтоны в основном позволяют вам иметь сложное глобальное состояние в языках, что в противном случае затрудняет или делает невозможным использование сложных глобальных переменных.
В частности, Java использует синглтоны в качестве замены глобальных переменных, поскольку все должно содержаться в классе. Наиболее близкими к глобальным переменным являются публичные статические переменные, которые можно использовать так, как если бы они были глобальными с import static
В C++ есть глобальные переменные, но порядок вызова конструкторов глобальных переменных класса не определен. Таким образом, синглтон позволяет вам отложить создание глобальной переменной до тех пор, пока эта переменная не понадобится в первый раз.
Такие языки, как Python и Ruby, очень мало используют синглтоны, потому что вместо них вы можете использовать глобальные переменные внутри модуля.
Так когда же хорошо / плохо использовать синглтон? Практически точно, когда было бы хорошо / плохо использовать глобальную переменную.
Современный C++ Design от Alexandrescu имеет поточно-ориентированный наследуемый универсальный синглтон.
Для моего 2p-достоинства я думаю, что важно определить время жизни ваших синглетонов (когда это абсолютно необходимо для их использования). Я обычно не даю статический get()
Функция создает все что угодно и оставляет настройку и уничтожение некоторому выделенному разделу основного приложения. Это помогает выделить зависимости между синглетонами, но, как подчеркивалось выше, лучше всего избегать их, если это возможно.
- Как правильно реализовать Singleton?
Есть одна проблема, о которой я никогда не упоминал, с которой я столкнулся на предыдущей работе. У нас были синглтоны C++, которые были разделены между DLL, и обычная механика обеспечения того, чтобы единственный экземпляр класса просто не работал. Проблема в том, что каждая DLL получает свой собственный набор статических переменных вместе с EXE. Если ваша функция get_instance встроенная или является частью статической библиотеки, каждая DLL будет иметь свою собственную копию "singleton".
Решение состоит в том, чтобы убедиться, что одноэлементный код определен только в одной DLL или EXE, или создать одноэлементный менеджер с этими свойствами для разделения экземпляров.
Как отмечали другие, основными недостатками синглетонов являются невозможность их расширения и утрата способности создавать экземпляры более чем одного экземпляра, например, для целей тестирования.
Некоторые полезные аспекты одиночных игр:
- ленивый или искренний экземпляр
- удобно для объекта, который требует настройки и / или состояния
Тем не менее, вам не нужно использовать синглтон, чтобы получить эти преимущества. Вы можете написать нормальный объект, который выполняет эту работу, и затем дать людям доступ к нему через фабрику (отдельный объект). Фабрика может беспокоиться только о том, чтобы создать его экземпляр, использовать его и т. Д., Если это необходимо. Кроме того, если вы программируете интерфейс, а не конкретный класс, фабрика может использовать стратегии, т.е. вы можете включать и выключать различные реализации интерфейса.
Наконец, фабрика поддается технологиям внедрения зависимостей, таким как Spring и т. Д.
Первый пример не является потокобезопасным - если два потока вызывают getInstance одновременно, то static будет PITA. Некоторая форма мьютекса поможет.
Singletons удобны, когда у вас много кода, запускаемого при инициализации и объекте. Например, когда вы используете iBatis при настройке объекта персистентности, он должен прочитать все конфиги, проанализировать карты, убедиться, что все правильно, и т. Д., Прежде чем перейти к вашему коду.
Если бы вы делали это каждый раз, производительность была бы сильно ухудшена. Используя его в одиночку, вы принимаете этот удар один раз, а затем все последующие вызовы не должны это делать.
Настоящим недостатком синглетонов является то, что они нарушают наследство. Вы не можете получить новый класс для расширенной функциональности, если у вас нет доступа к коду, на который ссылается Singleton. Таким образом, помимо факта, что Singleton сделает ваш код тесно связанным (это можно исправить с помощью паттерна стратегии... он же Dependency Injection), он также не позволит вам закрыть части кода от ревизии (совместно используемые библиотеки).
Так что даже примеры регистраторов или пулов потоков недопустимы и должны быть заменены стратегиями.
Большинство людей используют синглтоны, когда пытаются заставить себя чувствовать себя лучше при использовании глобальной переменной. Существуют законные варианты использования, но в большинстве случаев, когда люди используют их, тот факт, что может быть только один экземпляр, является тривиальным фактом по сравнению с тем фактом, что он доступен во всем мире.
Поскольку синглтон позволяет создавать только один экземпляр, он эффективно контролирует репликацию экземпляров. например, вам не понадобится несколько экземпляров поиска - например, карта поиска Морзе, поэтому можно поместить ее в одноэлементный класс. И только то, что у вас есть один экземпляр класса, не означает, что вы также ограничены количеством ссылок на этот экземпляр. Вы можете ставить в очередь вызовы (чтобы избежать проблем с потоками) к экземпляру и вносить необходимые изменения. Да, общая форма синглтона является общедоступной, вы можете изменить дизайн, чтобы создать синглтон с более ограниченным доступом. Я не устал это раньше, но я уверен, что это возможно. И всем тем, кто прокомментировал, что шаблон синглтона является абсолютно злым, вы должны знать это: да, это зло, если вы не используете его должным образом или в рамках него ограничены эффективной функциональностью и предсказуемым поведением: не ОБОБЩАЙТЕ.
Но когда мне нужно что-то вроде синглтона, я часто заканчиваю тем, что использую счетчик Шварца для его создания.
Ниже представлен лучший подход для реализации одноэлементного шаблона, безопасного для потоков, с освобождением памяти в самом деструкторе. Но я думаю, что деструктор должен быть необязательным, потому что экземпляр singleton будет автоматически уничтожен после завершения программы:
#include<iostream>
#include<mutex>
using namespace std;
std::mutex mtx;
class MySingleton{
private:
static MySingleton * singletonInstance;
MySingleton();
~MySingleton();
public:
static MySingleton* GetInstance();
MySingleton(const MySingleton&) = delete;
const MySingleton& operator=(const MySingleton&) = delete;
MySingleton(MySingleton&& other) noexcept = delete;
MySingleton& operator=(MySingleton&& other) noexcept = delete;
};
MySingleton* MySingleton::singletonInstance = nullptr;
MySingleton::MySingleton(){ };
MySingleton::~MySingleton(){
delete singletonInstance;
};
MySingleton* MySingleton::GetInstance(){
if (singletonInstance == NULL){
std::lock_guard<std::mutex> lock(mtx);
if (singletonInstance == NULL)
singletonInstance = new MySingleton();
}
return singletonInstance;
}
Относительно ситуаций, когда нам нужно использовать одноэлементные классы, можно: Если мы хотим поддерживать состояние экземпляра на протяжении всего выполнения программы, если мы вовлечены в запись в журнал выполнения приложения, где требуется только один экземпляр файла. быть использованы.... и так далее. Было бы заметно, если кто-нибудь может предложить оптимизацию в моем коде выше.
Я считаю их полезными, когда у меня есть класс, который содержит много памяти. Например, в недавней игре, над которой я работал, у меня есть класс карты влияния, который содержит коллекцию очень больших массивов непрерывной памяти. Я хочу, чтобы все выделялось при запуске, все освобождалось при выключении, и я определенно хочу только одну его копию. Я также должен получить доступ к нему из многих мест. Я считаю, что шаблон синглтона очень полезен в этом случае.
Я уверен, что есть и другие решения, но я считаю это очень полезным и простым в реализации.
Я использую Singletons в качестве собеседования.
Когда я прошу разработчика назвать некоторые шаблоны проектирования, если они могут назвать только Singleton, они не нанимаются.
Я до сих пор не понимаю, почему синглтон должен быть глобальным.
Я собирался создать синглтон, где я спрятал базу данных внутри класса как частную постоянную статическую переменную и создал функции класса, которые используют базу данных, даже не раскрывая базу данных пользователю.
Я не понимаю, почему эта функциональность была бы плохой.
Паттерн Мейерса синглтона работает достаточно хорошо в большинстве случаев, и в некоторых случаях он не обязательно стоит того, чтобы искать что-то лучше. Пока конструктор никогда не скинет и между синглетами нет никаких зависимостей.
Singleton - это реализация для глобально доступного объекта (GAO отныне), хотя не все GAO являются синглетонами.
Сами регистраторы не должны быть одиночными, но в идеале средства для регистрации должны быть глобально доступны, чтобы отделить, где генерируется сообщение журнала, откуда и как оно регистрируется.
Ленивая загрузка / отложенная оценка - это другое понятие, и синглтон, как правило, также реализует это. У него много собственных проблем, в частности, безопасности потоков и проблем, если он терпит неудачу с исключениями, так что то, что казалось хорошей идеей в то время, оказывается не таким уж великим. (Немного похоже на реализацию COW в строках).
Имея это в виду, GOA могут быть инициализированы следующим образом:
namespace {
T1 * pt1 = NULL;
T2 * pt2 = NULL;
T3 * pt3 = NULL;
T4 * pt4 = NULL;
}
int main( int argc, char* argv[])
{
T1 t1(args1);
T2 t2(args2);
T3 t3(args3);
T4 t4(args4);
pt1 = &t1;
pt2 = &t2;
pt3 = &t3;
pt4 = &t4;
dostuff();
}
T1& getT1()
{
return *pt1;
}
T2& getT2()
{
return *pt2;
}
T3& getT3()
{
return *pt3;
}
T4& getT4()
{
return *pt4;
}
Это не нужно делать так грубо, и явно в загруженной библиотеке, содержащей объекты, вы, вероятно, захотите, чтобы какой-то другой механизм управлял их временем жизни. (Поместите их в объект, который вы получите при загрузке библиотеки).
Что касается того, когда я использую синглтоны? Я использовал их для двух вещей - одноэлементная таблица, которая указывает, какие библиотеки были загружены с помощью dlopen; - обработчик сообщений, на который регистраторы могут подписаться, и на который можно отправлять сообщения. Требуется специально для обработчиков сигналов.
Anti-Usage:
Одна из основных проблем, связанных с чрезмерным использованием синглтонов, заключается в том, что шаблон предотвращает простое расширение и замену альтернативных реализаций. Имя класса жестко запрограммировано везде, где используется синглтон.
Если вы тот, кто создал синглтон и использует его, не делайте его синглтоном (это не имеет смысла, потому что вы можете контролировать особенность объекта, не делая его синглтоном), но это имеет смысл, когда вы разработчик библиотека, и вы хотите предоставить только один объект для ваших пользователей (в этом случае вы - тот, кто создал синглтон, но вы не пользователь).
Синглтоны - это объекты, поэтому используйте их как объекты, многие люди обращаются к синглетонам напрямую, вызывая метод, который его возвращает, но это вредно, потому что вы заставляете свой код знать, что объект одиночный, я предпочитаю использовать синглтоны в качестве объектов, я передаю их через конструктор, и я использую их как обычные объекты, таким образом, ваш код не знает, являются ли эти объекты одиночными или нет, и это делает зависимости более ясными, и это немного помогает для рефакторинга...
Я думаю, что это самая надежная версия для C#:
using System;
using System.Collections;
using System.Threading;
namespace DoFactory.GangOfFour.Singleton.RealWorld
{
// MainApp test application
class MainApp
{
static void Main()
{
LoadBalancer b1 = LoadBalancer.GetLoadBalancer();
LoadBalancer b2 = LoadBalancer.GetLoadBalancer();
LoadBalancer b3 = LoadBalancer.GetLoadBalancer();
LoadBalancer b4 = LoadBalancer.GetLoadBalancer();
// Same instance?
if (b1 == b2 && b2 == b3 && b3 == b4)
{
Console.WriteLine("Same instance\n");
}
// All are the same instance -- use b1 arbitrarily
// Load balance 15 server requests
for (int i = 0; i < 15; i++)
{
Console.WriteLine(b1.Server);
}
// Wait for user
Console.Read();
}
}
// "Singleton"
class LoadBalancer
{
private static LoadBalancer instance;
private ArrayList servers = new ArrayList();
private Random random = new Random();
// Lock synchronization object
private static object syncLock = new object();
// Constructor (protected)
protected LoadBalancer()
{
// List of available servers
servers.Add("ServerI");
servers.Add("ServerII");
servers.Add("ServerIII");
servers.Add("ServerIV");
servers.Add("ServerV");
}
public static LoadBalancer GetLoadBalancer()
{
// Support multithreaded applications through
// 'Double checked locking' pattern which (once
// the instance exists) avoids locking each
// time the method is invoked
if (instance == null)
{
lock (syncLock)
{
if (instance == null)
{
instance = new LoadBalancer();
}
}
}
return instance;
}
// Simple, but effective random load balancer
public string Server
{
get
{
int r = random.Next(servers.Count);
return servers[r].ToString();
}
}
}
}
Вот оптимизированная версия.NET:
using System;
using System.Collections;
namespace DoFactory.GangOfFour.Singleton.NETOptimized
{
// MainApp test application
class MainApp
{
static void Main()
{
LoadBalancer b1 = LoadBalancer.GetLoadBalancer();
LoadBalancer b2 = LoadBalancer.GetLoadBalancer();
LoadBalancer b3 = LoadBalancer.GetLoadBalancer();
LoadBalancer b4 = LoadBalancer.GetLoadBalancer();
// Confirm these are the same instance
if (b1 == b2 && b2 == b3 && b3 == b4)
{
Console.WriteLine("Same instance\n");
}
// All are the same instance -- use b1 arbitrarily
// Load balance 15 requests for a server
for (int i = 0; i < 15; i++)
{
Console.WriteLine(b1.Server);
}
// Wait for user
Console.Read();
}
}
// Singleton
sealed class LoadBalancer
{
// Static members are lazily initialized.
// .NET guarantees thread safety for static initialization
private static readonly LoadBalancer instance =
new LoadBalancer();
private ArrayList servers = new ArrayList();
private Random random = new Random();
// Note: constructor is private.
private LoadBalancer()
{
// List of available servers
servers.Add("ServerI");
servers.Add("ServerII");
servers.Add("ServerIII");
servers.Add("ServerIV");
servers.Add("ServerV");
}
public static LoadBalancer GetLoadBalancer()
{
return instance;
}
// Simple, but effective load balancer
public string Server
{
get
{
int r = random.Next(servers.Count);
return servers[r].ToString();
}
}
}
}
Вы можете найти этот шаблон на dotfactory.com.
В настольных приложениях (я знаю, только мы, динозавры, пишем их больше!) Они необходимы для получения относительно неизменных глобальных настроек приложения - языка пользователя, пути к файлам справки, пользовательских настроек и т. Д., Которые в противном случае должны были бы распространяться на каждый класс и каждый диалог.,
Изменить - конечно, они должны быть только для чтения!
Еще одна реализация
class Singleton
{
public:
static Singleton& Instance()
{
// lazy initialize
if (instance_ == NULL) instance_ = new Singleton();
return *instance_;
}
private:
Singleton() {};
static Singleton *instance_;
};