Проблемы с шаблоном Singleton

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

  • В среде сбора мусора может возникнуть проблема с управлением памятью.
  • В многопоточной среде это может вызвать узкие места и проблемы синхронизации.
  • Головная боль от тестирования.

Я начинаю задумываться над этими проблемами, но не полностью уверен в этих проблемах. Как и в случае сбоя в сборке мусора, использование статической в ​​одиночной реализации (что присуще шаблону), является ли это проблемой? Поскольку это будет означать, что статический экземпляр будет длиться до приложения. Это что-то, что ухудшает управление памятью (это просто означает, что память, выделенная для шаблона singleton, не будет освобождена)?

Конечно, в многопоточной настройке, если все потоки в конфликте для экземпляра singleton будут узким местом. Но как использование этого шаблона вызывает проблемы синхронизации (конечно, мы можем использовать мьютекс или что-то подобное для синхронизации доступа).

С точки зрения тестирования (единицы?), поскольку синглтоны используют статические методы (которые трудно подшутить или заострить), они могут вызвать проблемы. Не уверен в этом. Может кто-нибудь прокомментировать эту проблему тестирования?

Спасибо.

Ответ 1

В среде сбора мусора может возникнуть проблема с управлением памятью

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

Это большая проблема на языках, где у вас есть сборщик мусора (например, Java, Python и т.д.), потому что сборщик мусора всегда будет считать, что синглтон необходим. В С++ вы можете обманывать delete - указатель. Однако это открывает свою собственную червь из червей, потому что она должна быть одноэлементной, но, удалив ее, вы создаете вторую.

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

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

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

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

Головная боль от тестирования.

Примечательно, что одноэлементный конструктор можно тестировать только один раз. Вы должны создать совершенно новый набор тестов, чтобы снова проверить конструктор. Это нормально, если ваш конструктор не принимает никаких параметров, но как только вы примете пареметр, вы больше не сможете эффективно работать с тестом.

Кроме того, вы не можете вырезать синглтон так эффективно, и ваше использование макетных объектов становится трудным в использовании (есть способы обойти это, но это больше проблем, чем это стоит). Продолжайте читать больше об этом...

(И это приводит к плохому дизайну!)

Синглтоны также являются признаком плохого дизайна. Некоторые программисты хотят сделать свой класс базы данных singleton. "Наше приложение никогда не будет использовать две базы данных", - обычно думают они. Но придет время, когда имеет смысл использовать две базы данных или модульное тестирование, которое вы захотите использовать две разные базы данных SQLite. Если вы используете синглтон, вам придется внести серьезные изменения в ваше приложение. Но если вы использовали обычные объекты с самого начала, вы можете воспользоваться ООП, чтобы эффективно и своевременно выполнять свою задачу.

Большинство случаев singleton - результат ленивого программиста. Они не хотят передавать объект (например, объект базы данных) в кучу методов, поэтому они создают одноэлемент, который каждый метод использует как неявный параметр. Но этот подход укусил по причинам выше.

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

Ответ 2

Если вы еще не видели статью Singletons являются патологическими лжецами, вы тоже должны это прочитать. В нем обсуждается, как взаимосвязи между синглтонами скрыты от интерфейса, поэтому способ создания программного обеспечения также скрыт от интерфейса.

Есть ссылки на несколько других статей о одиночных играх того же автора.

Ответ 3

При оценке шаблона Singleton вы должны спросить: "Какая альтернатива? Будут ли те же проблемы, если я не использовал шаблон Singleton?"

В большинстве систем есть потребность в Big Global Objects. Это большие и дорогие предметы (например, диспетчеры подключения к базам данных) или хранят информацию о распространенном состоянии (например, информацию о блокировке).

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

Будут ли те же проблемы возникать в случае с не-синглетоном? Давайте рассмотрим их один за другим:

  • Управление памятью: Большой глобальный объект будет существовать, когда приложение будет запущено, и объект будет существовать до завершения работы. Поскольку существует только один объект, он будет занимать точно такой же объем памяти, как и одноэлементный. Использование памяти не является проблемой. (@MadKeithV: порядок уничтожения при выключении - это другая проблема).

  • Многопоточность и узкие места. Все потоки должны будут обращаться к одному и тому же объекту, независимо от того, переданы ли они этому объекту в качестве параметра или вызываются ли они MyBigGlobalObject.GetInstance(). Итак, Singleton или нет, у вас все равно будут проблемы с синхронизацией (к счастью, к ним относятся стандартные решения). Это тоже не проблема.

  • Тестирование модулей. Если вы не используете шаблон Singleton, вы можете создать Big Global Object в начале каждого теста, и сборщик мусора уберет его, когда тест завершается. Каждый тест начнется с новой чистой среды, которая не была подвергнута предыдущему тесту. Альтернативно, в случае Singleton один объект проходит через ВСЕ тесты и может легко "загрязняться". Итак, шаблон Singleton действительно кусает, когда дело доходит до модульного тестирования.

Мое предпочтение: из-за проблемы с единичным тестированием я стараюсь избегать шаблона Singleton. Если это одна из немногих сред, в которой у меня нет модульного тестирования (например, уровня пользовательского интерфейса), тогда я мог бы использовать Singletons, в противном случае я избегаю их.

Ответ 4

Мой главный аргумент против синглетонов в основном состоит в том, что они объединяют два плохих свойства.

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

Более фундаментальная проблема с синглтонами заключается в том, что то, что они пытаются сделать, в корне плох.

Синтаксис, как определено GoF, имеет два свойства:

  • Он доступен по всему миру, и
  • Это предотвращает создание экземпляра более чем один раз.

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

Вторая проблема менее очевидна, но принципиально она пытается решить несуществующую проблему.

Когда вы в последний раз случайно создали экземпляр класса, где вместо этого вы планировали повторно использовать существующий экземпляр?

Когда вы в последний раз случайно набрали "std::ostream() << "hello world << std::endl", когда вы имели в виду "std::cout << "hello world << std::endl"?

Этого просто не бывает. Поэтому нам не нужно это предотвращать в первую очередь.

Но что более важно, ощущение, что "только один экземпляр должен существовать" почти всегда ошибочен. То, что мы обычно подразумеваем, это "В настоящее время я вижу только использование для одного экземпляра".

но "я могу видеть только использование для одного экземпляра" - это не то же самое, что "приложение будет рушиться, если кто-то посмеет создать два экземпляра".

В последнем случае одноэлемент может быть оправдан. но в первом, это действительно преждевременный выбор дизайна.

Обычно мы получаем более одного экземпляра.

Вы часто нуждаетесь в более чем одном регистраторе. Там в журнале вы пишете чистые, структурированные сообщения, чтобы клиент контролировал, и там вы отправляете отладочные данные для вашего собственного использования.

Также легко представить, что вы можете использовать несколько баз данных.

Или настройки программы. Конечно, только один набор настроек может быть активным одновременно. Но пока они активны, пользователь может войти в диалог "Параметры" и настроить второй набор параметров. Он еще не применил их, но после того, как он набрал "ok", их нужно поменять местами и заменить текущий активный набор. И это означает, что до тех пор, пока он не ударит "ok", на самом деле существуют два набора опций.

И в целом, модульное тестирование:

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

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

Таким образом, проблема с синглтонами - это не технические проблемы, такие как "трудно получить правильность потока", но гораздо более фундаментальные "они фактически не вносят ничего позитивного в ваш код. Они добавляют две черты, каждая из которых они отрицательные, к вашей кодовой базе. Кто когда-нибудь захочет этого?"

Ответ 5

Об этой проблеме с тестированием устройств. Главные проблемы, похоже, не в тестировании самих синглтонов, а при тестировании объектов, которые используют.

Такие объекты не могут быть изолированы для тестирования, поскольку они имеют зависимости от одиночных, которые скрыты и трудно удаляются. Это становится еще хуже, если singleton представляет собой интерфейс к внешней системе (соединение БД, процессор платежей, блок обжига МБР). Тестирование такого объекта может неожиданно записываться в БД, отправлять деньги, которые знают, где или даже запускают некоторые межконтинентальные ракеты.

Ответ 6

Я согласен с более ранними настройками, которые часто используются, поэтому вам не нужно передавать аргумент повсюду. Я делаю это. Типичным примером является ваш системный журнал. Я обычно делаю это синглом, поэтому мне не нужно передавать его по всей системе.

Обзор. В примере объекта ведения журнала, сколько из вас (показ рук) добавит дополнительный аргумент в какую-либо подпрограмму, которая может потребоваться для регистрации чего-либо -vs-use singleton?

Ответ 7

Я бы не обязательно приравнивал Singletons к Globals. Ничто не должно препятствовать разработчику передавать экземпляр объекта, singleton или иначе, в качестве параметра, а не вызывать его из эфира. Цель скрыть свою глобальную доступность можно было бы сделать, спрятав функцию getInstance нескольким избранным друзьям.

Что касается дефекта тестирования устройства, то Unit означает маленький, поэтому повторное обращение к приложению для проверки singleton по-другому кажется разумным, если только я не упустил что-то в этом очко.