Pull to refresh

Comments 25

Имхо использование enum для синглетон - это костыль и анти-паттерн, хотя работает отлично.

Также мне нравится концепция Spring, где класс отвечает за то, что он должен делать, а не то как он инстанцируется. Singleton или нет - это вынесено: Spring bean scopes.

Ох не соврать бы, вроде enum Singleton были "нормальными" в java 1.4 или 1.5. И я сейчас даже не могу припомнить почему. В 2024 году сам шаблон Singleton основательно потускнел, ну или по крайней мере его смысл поменялся (говорим спасибо облакам, кластерам, докеру). Как верно написал @n43jl сейчас мы мыслим категориями scope (контекст). Контекст, а в 99% случаях это Spring Context, сам определяет кого, сколько и в каких случаях инстанцировать. Даже зная что скоуп по умолчанию в спринге Singleton, при проектировании приложений нам необходимо закладывать множество инстансов приложения запущенных параллельно.

С моей колокольни Singleton был очень популярен 15-20 лет назад, потому что очень часто не нужно больше одного инстанса и все писали свои велосипеды: Logger, DB connection pool, Caching, Any State Management, File System Management, etc. И к сожалению написать правильно синглетон, который будет работать в многопоточке - не так уж и просто, от туда и пошли ужасные костыли вида Double-Checked-Locking Singleton о которых меня даже спрашивали на собеседовании в районе 2010. Enum - решает все вопросы многопоточки, где single-instance гарантируется JVM. Более того использовать Enum для синглетона - это рекомендация из книги Effective Java (имхо анти-рекомендация).

Но потом года с 2010 начали говорить про зло Global state, про Testability, и Spring начал становиться довольно популярным - Singleton стал анти-паттерном.

Более того использовать Enum для синглетона - это рекомендация из книги Effective Java (имхо анти-рекомендация).

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

Spring начал становиться довольно популярным

И теперь его, не думая, пихают везде где нужно и где не нужно.

Singleton стал анти-паттерном

И именно поэтому его использует вышеупомянутый Spring? Синглтон - это просто объект-одиночка в рамках, как правило, приложения. Вне зависимости от механизма реализации данного поведения, будь-то spring, CDI, micronaut, etc.

В том что вы предлагаете также несколько других проблем:

  • Сломанная семантика: то что мы делаем синглетоном, очень часто никакого отношения к enumeration не имеет

  • Double responsibility - мы привязываем сервис и его жизненный цикл в один coupled кусок

И теперь его, не думая, пихают везде где нужно и где не нужно.

Это совершенно другой топик

Синглтон - это просто объект-одиночка в рамках, как правило, приложения. Вне зависимости от механизма реализации

Это понятно. Имхо анти-паттерн - это не class-lifecycle-management, а анти-паттерн связывание бизнес-логики (сервиса) и class-lifecycle-management вместе. Поэтому как GoF его преподносит, и как Effective Java рекомендует делать синглетоны - это имхо антипаттерн. А вот именно Spring class-lifecycle-management - это то, что позволяет нормально тестировать бизнес-логику не привязываясь сколько инстансов реально будет в проде.

очень часто никакого отношения к enumeration не имеет

Именно поэтому enum приватный и напрямую наружу не торчит, потому что его использование обусловлено лишь гарантией единственности экземпляра со стороны самой jvm.
Например в гугловой гуаве до появления лямбд через подобный механизм реализованы некоторые функции (Functions.toStringFunction(), Functions.identity(), ...)

GoF его преподносит

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

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

как Effective Java рекомендует делать синглетоны

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

public class SingletonImpl {

  private static class Holder {
    private static final SingletonImpl HOLDER_INSTANCE = new SingletonImpl();
  }

  public static SingletonImpl getInstance() {
    return Holder.HOLDER_INSTANCE;
  }

  private SingletonImpl() {}
  
  public void doSomething() {}
  
}

а этот вариант через класс холдер чем плох?

Зачем?)

тут фишка в безопасной инициализации как минимум

ну и это просто красивый вариант, пожалуй лучший из возможных

HOLDER_INSTANCE создается и инициализируется в момент загрузки класса?

При первом обращении

Этот вариант плох тем, что у него double-responsibilty:

  • бизнес логика сервиса

  • instantiation/lifecycle management

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

В 2024 году сам шаблон Singleton основательно потускнел

LoggerFactory.getLogger(MyService.class) из большинства активно используемых логеров передаёт привет.

да практически любой Config с get only методами точно также передает привет

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

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

Я не говорю, что приложению нужно несколько инстансов конфигов, я говорю что как gof, effective Java, предлагают это делать - это антипаттерн создающий проблемы в тестировании

Конфиги как синглетон - это проблема для тестирования: одному тесту ты передашь один конгфиг, другому второй, третьему третий. И ранишь все эти тесты параллельно, как это будет работать с синглетоном?

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

На эту проблему можно смотреть с разных углов. Вы ее формулируете так, я бы скорее ее сформулировал, что: "Глобальное требование иметь один инстанс протекает в бизнес логику конкретного сервиса, которому не имеет значение сколько инстансов ранится". Что вытекает из-того, что "догматичная" имплементация синглетона - это double-responsibility: strong-coupling бизнес логики и lifecycle/instantiating management. Если их разделить и сделать single-responsibility, то и не будет проблем протестировать отдельно бизнес логику в столько потоков во сколько душе угодно, и протестировать синглетон в интеграционных тестах, что он синглетон.

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

И конфиги - синглетоны, это чистой воды оптимизация ресурсов. Ничего не поменяется если конфиг просто будет Immutable data class, и мы будем создавать новый такой же класс по какому-нибудь чиху.

Если конфиги реализованы синглетоном как предлагают GoF или Effective Java, то это протекание оптимизации ресурсов приложения в бизнес логику. И вообще то, что я считаю анти-паттерном синглетона - это протекание "lifecycle management" в бизнес логику.

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

Подскажите пожалуйста, а что это за приложения, которые требуют изменения тестовой среды, во время работы этого приложения, и при этом, не требуют смены настроек в режиме продуктовой эксплуатации?

Если я правильно понимаю, enum Singleton не ленивый (как и final Singleton).

блокировка с двойной проверкой (имейте в виду, что это может работать не так, как ожидается, из-за оптимизаций компилятора)

Все будет работать именно так, как ожидается, если понимать что именно нужно ожидать. Вот статья, которая ответит на все вопросы: https://shipilev.net/blog/2014/safe-public-construction/

EnumSingleton.INSTANCE.getInstance();

Что за бредятина? Зачем нужен getInstance(), ежели и так имеется доступ к экземпляру?

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

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

Также в статье не обсуждено создание синглетонов thread-local, хотя это крайне необходимо. Об этом, кстати, в статье упоминается, но не развивается - о проблемах синглетонов в контексте потоков.

Имхо, давно пора в Java на уровне языка ввести соответствующую конструкцию.

Sign up to leave a comment.