Одиночка (шаблон проектирования)

Материал из Википедии — свободной энциклопедии
(перенаправлено с «Singleton»)
Перейти к навигации Перейти к поиску
Одиночка
Singleton
Тип порождающий
Плюсы организует API; неявно загружает нужные модули в нужном порядке; оставляет место для второго похожего объекта
Минусы усложняет тестирование, многопоточность и отслеживание задержек; одиночки не должны неявно зависеть друг от друга
Описан в Design Patterns Да

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

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

Глобальный «одинокий» объект — именно объект (log().put("Test");), а не набор процедур, не привязанных ни к какому объекту (logPut("Test");) — бывает нужен, если:

  • Используется существующая объектно-ориентированная библиотека и ей нужен объект, унаследованный от определённого класса/интерфейса.
  • Есть шансы, что один объект когда-нибудь превратится в несколько[1].
  • Дополнительные факторы, исполнимые в обеих концепциях, но хорошо сочетающиеся с методикой ООП:
    • Интерфейс объекта (например, игрового мира) слишком сложен, и объект/префикс log служит для организации API.
    • В зависимости от каких-нибудь условий и настроек, создаётся один из нескольких объектов. Например, в зависимости от того, ведётся лог или нет, создаётся настоящий объект, пишущий в файл, или «заглушка», ничего не делающая.
    • Создание объекта занимает время, и для красоты объект можно создавать, когда на экране уже что-то видно.

Такие объекты можно создавать и при инициализации программы. Это может приводить к следующим трудностям:

  • Если объект нужен уже при инициализации, он может быть затребован раньше, чем будет создан.
  • Бывает, что объект нужен не всегда. В таком случае его создание можно пропустить. Особенно это важно, если одиночек (например, диалоговых окон) много — тогда пользователь быстро получит интерфейс, а окна будут создаваться по одному, не мешая работе пользователя.

Одиночка может принадлежать и не глобальному пространству имён, а какому-то объекту — например, главной форме Qt.

  • Наведение порядка в глобальном пространстве имён.
  • Ускорение начального запуска программы, если есть множество одиночек, которые не нужны для запуска. Особенно удачно выходит, если создание всех «одиночек» даёт ощутимую задержку, а создание каждого отдельного — практически незаметно.
    • Ускоряется и неуправляемая инициализация программы (до тела): будут запущены только те модули, которые действительно там нужны. А когда тело получит управление, можно хотя бы нарисовать заставку.
  • Упрощение кода инициализации — система автоматически неявно отделит нужные компоненты от ненужных и проведёт топологическую сортировку.
  • Одиночку можно в дальнейшем превратить в шаблон-стратегию или несколько таких объектов[1].
    • Пример шаблона-стратегии: запись журнала действий в файл или в никуда.
    • Пример нескольких объектов: размножив классы Player и Renderer, можно сделать игру вдвоём на одной машине.
  • Усложняется контроль за межпоточными гонками и задержками.
    • Многопоточного «одиночку» сложно писать «из головы»[1][2]: доступ к давно построенному одиночке в идеале не должен открывать мьютекс. Лучше проверенные решения. Как пример см. преамбулу к статье Модель памяти Java.
    • Конфликт двух потоков за недостроенного одиночку приведёт к задержке.
    • Если объект создаётся долго, задержка может мешать пользователю или ещё как-то нарушать реальное время. В таком случае его создание лучше перенести в старт программы.
    • Если программа стартует долго, сложнее становится сделать строку прогресса.
  • Сам факт, что две далёкие друг от друга функции неявно полагаются на одного одиночку, может оказаться плохой архитектурой[2].
  • Требуются особые функции для модульного тестирования, чтобы физически изолировать тесты[3] — например, сделать менеджер одиночек не единственным[3]. Впрочем, одиночками часто являются модули общения с аппаратурой, объектами ОС[1], программным окружением и пользователем, которые модульному тестированию поддаются плохо.
  • Требуется особая тактика тестирования готовой программы, ведь пропадает даже понятие «простейшая запускаемость» — запускаемость зависит от конфигурации.
  • Маленький объект без данных — чистый шаблон-стратегию или null object — обычно держат в сегменте данных, а не в динамической памяти, и превращают в одиночку в особых случаях.
  • Сами по себе одиночки никак не заведуют порядком выгрузки. Возможна даже ситуация «сборщик мусора уничтожил одиночку, клиент создал нового»[1]. Некоторые языки (Паскаль) гарантируют порядок уничтожения модулей, а если нет — можно его обеспечить сторонними средствами, или при выходе явно прекратить всю подозрительную деятельность.
  • Компоненты не должны иметь неявных связей между собой, иначе небольшое изменение — в программном коде, файле настроек, сценарии пользования — может спутать порядок и вызвать трудноуловимую ошибку. Пример: одиночка А использует COM, но полагается на CoInitialize, вызванный одиночкой Б, и без него работать не может. Решение: сделать одиночку CoInit, который явно используется и А, и Б.
    • Одиночки требуют особого внимания, если один из компонентов заведомо ненадёжен и для адекватной работы требует особых условий («разглючек»): библиотека, которая иногда портит память; сеть, которая разрывает соединение, если слишком долго ждать; типографский движок, способный загрузить шрифты в одном порядке и не способный наоборот… Тогда, в зависимости от порядка инициализации, компонент может сработать адекватно или нет.

Применение

[править | править код]
  • должен быть ровно один экземпляр некоторого класса, легко доступный всем клиентам;
  • единственный экземпляр должен расширяться путём порождения подклассов, и клиентам нужно иметь возможность работать с расширенным экземпляром без модификации своего кода.

Возможные замены

[править | править код]
  • Просто создать объект при запуске[2]. Если объект гарантированно нужен всегда, можно и не делать его одиночкой.
  • Внедрение зависимости — создать объект, а потом как-то передать его всем, кто им пользуется[2][4].

Примеры использования

[править | править код]
  • Ведение отладочного файла для приложения.
  • В любом приложении для iOS существует класс AppDelegate, реагирующий на системные события.

Примеры реализации

[править | править код]

Из PEP 0318 Архивная копия от 3 июня 2020 на Wayback Machine:

Из PEP 0318 Архивная копия от 3 июня 2020 на Wayback Machine[нет в источнике]:

Ниже приведена одна из возможных реализаций паттерна Одиночка на C++ (известная как синглтон Майерса), где одиночка представляет собой статический локальный объект. Важным моментом является то, что конструктор класса объявлен как private, что позволяет предотвратить создание экземпляров класса за пределами его реализации. Помимо этого, закрытыми также объявлены конструктор копирования и оператор присваивания. Последние следует объявлять, но не определять, так как это позволяет в случае их случайного вызова из кода получить легко обнаруживаемую ошибку компоновки. Отметим также, что приведенный пример не является потокобезопасным в C++03, для работы с классом из нескольких потоков нужно защитить переменную theSingleInstance от одновременного доступа, например, с помощью мьютекса или критической секции. Впрочем, в C++11 синглтон Майерса является потокобезопасным и без всяких блокировок.

Ещё один пример реализации одиночки на C++ с возможностью наследования для создания интерфейса, каркасом которого послужит, собственно, одиночка. Временем «жизни» единственного объекта удобно управлять, используя механизм подсчета ссылок.

Для Delphi 2005 и выше подходит следующий пример (не потоко-безопасный):

Для более ранних версий следует переместить код класса в отдельный модуль, а объявление Instance заменить объявлением глобальной переменной в его секции implementation (до Delphi 7 включительно секции class var и strict private отсутствовали).

Литература

[править | править код]
  • Алан Шаллоуей, Джеймс Р. Тротт Шаблоны проектирования. Новый подход к объектно-ориентированному анализу и проектированию = Design Patterns Explained: A New Perspective on Object-Oriented Design. — М.: «Вильямс», 2002. — С. 288. — ISBN 0-201-71594-5.
  • Эрик Фримен, Элизабет Фримен. Паттерны проектирования = Head First Design Patterns. — СПб.: Питер, 2011. — 656 с. — ISBN 978-5-459-00435-9.

Примечания

[править | править код]
  1. 1 2 3 4 5 Источник. Дата обращения: 21 ноября 2023. Архивировано 21 ноября 2023 года.
  2. 1 2 3 4 Drawbacks of the Singleton Design Pattern | Baeldung
  3. 1 2 How to write unit tests for a singleton class | Technical Feeder. Дата обращения: 21 ноября 2023. Архивировано 21 ноября 2023 года.
  4. https://medium.com/@shashidj206/the-singleton-design-pattern-best-practices-and-pitfalls-58f9d1c17ab7