Допустимые глобальные переменные и предполагаемая экономия памяти.

Вот уже 20 лет я преподаю программирование в университете Буэнос-Айреса. На курсе программной инженерии мы изучаем паттерны проектирования, и одна и та же «схема» повторяется раз за разом, вызывая почти дежавю. Я убедился в этом на нескольких проектах и при обращении со свободным ПО, которым мне приходилось пользоваться:

Как «по волшебству» в коде возникает паттерн синглтон.

Источник зла

Этот паттерн применяется в отрасли десятилетиями. Его популярность связывают с отличной книгой «Паттерны объектно-ориентированного проектирования». Синглтон используется во множестве фреймворков, а в литературе редко встречаются рекомендации его избегать. Несмотря на это, в соответствующей статье Википедии находим предупреждение в стиле Данте:

«Критики синглтона рассматривают синглтон как антипаттерн, часто используемый в таких сценариях, где он скорее вреден».

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

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

Почему его не стоит использовать

1. Он нарушает принцип биекции

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

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

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

https://mcsee.hashnode.dev/the-one-and-only-software-design-principle

2. Он вызывает глобальную связность

Это глобальная ссылка. Опять же, согласно Википедии:

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

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

https://mcsee.hashnode.dev/coupling-the-one-and-only-software-design-problem

3. В нём много говорится о (случайных) деталях реализации и мало о (существенной) зоне ответственности

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

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

<?
	

	class God {
	    private static $instance = null;
	

	    private function __construct() { }
	

	    public static function getInstance() {
	    if (null === self::$instance) {
	        self::$instance = new self();
	    }
	    return self::$instance;
	   }
	}

4. Он не даёт писать хорошие модульные тесты

Из вышеописанного связывания вытекает следующая проблема: невозможность полностью контролировать побочные эффекты теста, а значит — невозможность гарантировать, что он будет детерминированным.

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

5. Не экономится пространство в памяти

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

В таких виртуальных машинах, применяемых в большинстве современных языков, оказывается гораздо затратнее хранить объекты в области памяти, которую алгоритм сборки мусора проходит дважды (mark & ​​sweep), чем создавать временные объекты, а затем быстро их удалять.

6. Синглтон не позволяет использовать внедрение зависимостей

Как пропагандируется в рамках надёжного проектирования, во избежание связывания мы предпочитаем инверсию управления, достигаемую через внедрение зависимостей.

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

7. Он нарушает соглашение о создании экземпляров

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

<?
	

	final class God extends Singleton {
	}
	

	$christianGod = new God();

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

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

<?
	

	class Singleton {
	    private function __construct() {
	        throw new Exception('Cannot Create new instances');
	    }
	}

8. Мы вынуждены явно связываться с реализацией

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

<?
	

	$christianGod = God::getInstance();
	// Почему мы должны быть в курсе о getInstance, когда создаем объект ?

9. Он мешает создавать автоматизированные тесты

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

Если в соответствии с правилами бизнес-логики у нас должен быть всего один провайдер определённого сервиса, то он будет моделироваться через контролируемую точку доступа (в роли которой не должен использоваться глобальный класс, а тем более синглтон).

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

10. Уникальные концепции зависят от контекста

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

Структура паттерна в соответствии с определением, приводимым в книге «Паттерны проектирования»
Структура паттерна в соответствии с определением, приводимым в книге «Паттерны проектирования»
Класс (и вся модель) в биекции отсутствуют. Соответственно, все отношения, связанные с данным классом, будут недействительными
Класс (и вся модель) в биекции отсутствуют. Соответственно, все отношения, связанные с данным классом, будут недействительными

11. С ним сложно справиться в многопоточных окружениях

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

12. Накапливается мусор, занимающий место в памяти

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

13. Такое состояние с накоплением мусора недопустимо при применении модульных тестов

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

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

14. Если налагается ограничение на создание новых объектов, то нарушается принцип единственной ответственности.

Принцип единственной ответственности класса заключается в создании экземпляров. Если наделить класс любой другой ответственностью, это нарушит принцип единственной ответственности. Класс не должно волновать, является ли он синглтоном. Всё, что он должен делать — строго соблюдать правила бизнес-логики. Если существует потребность, чтобы некоторые экземпляры были уникальными, то за это будет отвечать третий объект-посредник, например, Фабрика или Строитель.

15. За обладание глобальной ссылкой мы расплачиваемся не только связностью

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

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

16. Вероятно, он просто проник сюда за компанию

Многими синглтонами как таковыми злоупотребляют, превращая их в глобальные репозитории ссылок

Очень велик соблазн использовать синглтон как точку входа для новых ссылок.

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

Мало того, что синглтон — корень всех зол, так он ещё и легко проникает в код за компанию. В больших проектах в синглтоне просто накапливается мусор — чтобы не мешал.

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

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

Известно несколько примеров анализа первопричин (Root Cause Analysis) и постмортемов, демонстрирующих, как дефекты продукции коррелируют с состоянием систем и х недотестированностью.

https://blog.ndepend.com/singleton-design-pattern-impact-quantified/

18. Это запах кода

https://maximilianocontieri.com/code-smell-32-singletons

Для чего может понадобиться синглтон

Сформулировав аргументы против использования синглтона, давайте попробуем рассмотреть его достоинства:

1. Этот паттерн помогает экономить память

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

2. Он хорош для моделирования уникальных объектов

При помощи синглтона можно гарантировать уникальность некоторой концепции. Но это не единственный и не лучший способ. Давайте перепишем предыдущий пример:

<?
	

	interface Religion {
	    // Определим общее для всех религий поведение
	}
	

	final class God {
	    // Для различных религий характерны разные убеждения
	}
	

	final class PolythiesticReligion implements Religion {
	    private $gods;
	

	    public function __construct(Collection $gods) {
	        $this->gods = $gods;
	    }
	}
	

	final class MonotheisticReligion implements Religion {
	    private $godInstance;
	

	    public function __construct(God $onlyGod) {
	        $this->godInstance = $onlyGod;
	    }
	}
	

	// Согласно христианству и некоторым другим религиям,
	// существует всего один Бог.
	// Это не соблюдается в других религиях.
	

	$christianGod = new God();
	$christianReligion = new MonotheisticReligion($christianGod);
	// В таком контексте Бог уникален
	// Невозможно создать нового Бога или изменить имеющегося
	// Это сущность с глобальной областью действия
	

	$jupiter = new God();
	$saturn = new God();
	$mythogicalReligion = new PolythiesticReligion([$jupiter, $saturn]);
	

	// Боги могут быть уникальными (или нет) в зависимости от контекста
	// Можно создавать тестовые религии, обладающие либо не обладающие показателем уникальности Бога
	// Этот код менее тесно связан, 
	// поскольку мы разрываем прямую ссылку на класс God 
	// Единственная ответственность класса God — создавать богов 
	// Но не управлять ими

Создание единственного экземпляра и управление им не связаны. Уникальность творения осуществляется в одной контролируемой точке, ссылки на классы от неё откреплены.

3. Синглтон позволяет не повторять затратные инициализации

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

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

Решение

Существует ряд приёмов, позволяющих постепенно вывести синглтоны из употребления и не злоупотреблять ими. Некоторые из них перечислены в следующей статье:

https://mcsee.hashnode.dev/how-to-decouple-a-legacy-system

Заключение

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

P.S. Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.

Комментарии (11)


  1. AstarothAst
    17.01.2025 12:52

    Разработчики на Spring:

    - Синглтон зло? Ух ты!


  1. php7
    17.01.2025 12:52

    Автор теоретик?


    1. piton_nsk
      17.01.2025 12:52

      Вот уже 20 лет я преподаю программирование

      Он самый.


  1. Revertis
    17.01.2025 12:52

    Автор предъявляет слишком странные требования порой, а реально нужные случаи использования просто упускает.


  1. GospodinKolhoznik
    17.01.2025 12:52

    Дети, запомните, синглтон это плохо. Пнятненько?


  1. ProgerMan
    17.01.2025 12:52

    Мне показалось, или все беды в мире от синглтонов?
    Глобальное потепление, получается, тоже своего рода синглтон?


  1. flancer
    17.01.2025 12:52

    Вот за что мне симпатично функциональное программирование, так это за то, что там все функции - синглтоны. Раз у них нет состояния, то на всё приложение нужна ровно одна функция, выполняющая конкретную работу. И никто её демоном не считает.

    Ну и даёт Maximiliano Contieri жару в универе Буэнос-Айреса!!


  1. keekkenen
    17.01.2025 12:52

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

    после этой фразы можно дальше не читать

    наверное, вы просто не знаете чего хотите или рандомно решаете, что будет синглтоном, а что нет..


  1. NeoNN
    17.01.2025 12:52

    В анемичной модели с сервисами зачастую подвязанные через DI сервисы - это определенные как single instance сущности, и никакого смысла их делать чем-то иным нет.


  1. gelioson
    17.01.2025 12:52

    Синглтон — корень всех зол

    Только Ситхи все возводят в абсолют


  1. BugM
    17.01.2025 12:52

    Spring и все что он принес в массы вообще мимо вас прошло?