Почему синглтон антипаттерн
Перейти к содержимому

Почему синглтон антипаттерн

Why is Singleton considered an anti-pattern? [duplicate]

I’ve heard recently that Singleton is an anti-pattern. I know it has to do with the fact making a class singleton is like making that unique instance a global variable, but it’s also doing a lot more than that (limiting the number of instances of that object, managing instantiation, etc..).

Why exactly is Singleton considered an anti-pattern? And what are the alternatives?

4 Answers 4

To help with answering, here is more about the anti-pattern comment:

it is overused, introduces unnecessary restrictions in situations where a sole instance of a class is not actually required, and introduces global state into an application

Here is a great ending to the blog above:

In short, the singleton pattern makes code more complex, less useful, and a real pain to re-use or test. Eliminating singletons can be tricky, but it’s a worthwhile endeavour.

OK, so, the reason it is an anti-pattern is described well in this paragraph, and, as the author expresses, it tightly couples your code to the singleton.

If you find that you want to use a singleton, you may want to consider your design, but there are times where it is useful.

For example, once I had to write an application that could have at most one database connection, to process thousands of requests. So, a singleton makes sense since I am resource constrained to having only one instance.

But, generally this is used to simplify code, without thinking of the difficulties that will be introduced.

For example, and this applies to static classes also, if you unit test, or have concurrency, then the state of one request will change the state and that may cause problems, as the class calling the instance may be assuming the state is as it expected.

I think the best way to challenge the use is to think of how to handle it if your program is multi-threaded, and a simple way to do that is to unit test it, if you have several tests that run at one time.

If you find that you still need it, then use it, but realize the problems that will be encountered later.

Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью

Обложка: Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью

Паттерн «Одиночка» — пожалуй, самый известный паттерн проектирования. Тем не менее, он не лишен недостатков, поэтому некоторые программисты (например, Егор Бугаенко) считают его антипаттерном. Разбираемся в том, какие же подводные камни таятся в Singleton’е.

Определение паттерна

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

Простейшая реализация

Как уже говорилось выше, в этом нет ничего сложного:

  • Сделайте конструктор класса приватным, чтобы не было возможности создать экземпляр класса извне.
  • Храните экземпляр класса в private static поле.
  • Предоставьте метод, который будет давать доступ к этому объекту.

Принцип единственной обязанности

В объектно-ориентированном программировании существует правило хорошего тона — «Принцип едиственной обязанности» (Single Responsibility Principle, первая буква в аббревиатуре SOLID). Согласно этому правилу, каждый класс должен отвечать лишь за один какой-то аспект. Совершенно очевидно, что любой Singleton-класс отвечает сразу за две вещи: за то, что класс имеет лишь один объект, и за реализацию того, для чего этот класс вообще был создан.

Принцип единственной обязанности был создан не просто так — если класс отвечает за несколько действий, то, внося изменения в один аспект поведения класса, можно затронуть и другой, что может сильно усложнить разработку. Так же разработку усложняет тот факт, что переиспользование (reusability) класса практически невозможно. Поэтому хорошим шагом было бы, во-первых, вынести отслеживание того, является ли экземпляр класса единственным, из класса куда-либо во вне, а во-вторых, сделать так, чтобы у класса, в зависимости от контекста, появилась возможность перестать быть Singleton’ом, что позволило бы использовать его в разных ситуациях, в зависимости от необходимости (т.е. с одним экземпляром, с неограниченным количество экземпляров, с ограниченным набором экземпляров и так далее).

Тестирование

Один из главных минусов паттерна «Одиночка» — он сильно затрудняет юнит-тестирование. «Одиночка» привносит в программу глобальное состояние, поэтому вы не можете просто взять и изолировать классы, которые полагаются на Singleton. Поэтому, если вы хотите протестировать какой-то класс, то вы обязаны вместе с ним тестировать и Singleton, но это ещё полбеды. Состояние «Одиночки» может меняться, что порождает следующие проблемы:

  • Порядок тестов теперь имеет значение;
  • Тесты могут иметь нежелательные сторонние эффекты, порождённые Singleton’ом;
  • Вы не можете запускать несколько тестов параллельно;
  • Несколько вызовов одного и того же теста могут приводить к разным результатам.

На эту тему есть отличный доклад с «Google Tech Talks»:

Скрытые зависимости

Обычно, если классу нужно что-то для работы, это сразу понятно из его методов и конструкторов. Когда очевидно, какие зависимости есть у класса, гораздо проще их предоставить. Более того, в таком случае вы можете использовать вместо реально необходимых зависимостей заглушки для тестирования. Если же класс использует Singleton, это может быть совершенно не очевидно. Всё становится гораздо хуже, если экземпляру класса для работы необходима определённая инициализация (например, вызов метода init(. ) или вроде того). Ещё хуже, если у вас существует несколько Singleton’ов, которые должны быть созданы и инициализированы в определённом порядке.

Загрузчик класса

Если говорить о Java, то обеспечение существования лишь одного экземпляра класса, которое так необходимо для Singleton, становится всё сложнее. Проблема в том, что классическая реализация не проверяет, существует ли один экземпляр на JVM, он лишь удостоверяется, что существует один экземпляр на classloader. Если вы пишете небольшое клиентское приложение, в котором используется лишь один classloader, то никаких проблем не возникнет. Однако если вы используете несколько загрузчиков класса или ваше приложение должно работать на сервере (где может быть запущено несколько экземпляров приложения в разных загрузчиках классов), то всё становится очень печально.

Десериализация

Ещё один интересный момент заключается в том, что на самом деле стандартная реализация Singleton не запрещает создавать новые объекты. Она запрещает создавать новые объекты через конструктор. А ведь существуют и другие способы создать экземпляр класса, и один из них — сериализация и десериализация. Полной защиты от намеренного создания второго экземпляра Singleton’а можно добиться только с помощью использования enum’а с единственным состоянием, но это — неоправданное злоупотребление возможностями языка, ведь очевидно, что enum был придуман не для этого.

Потоконебезопасность

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

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

  • Первый поток обращается к getInstance() , когда объект ещё не создан;
  • В это время второй тоже обращается к этому методу, пока первый ещё не успел создать объект, и сам создаёт его;
  • Первый поток создаёт ещё один, второй, экземпляр класса.

Разумеется, можно просто пометить метод как synchronised , и эта проблема исчезнет. Проблема заключается в том, что, сохраняя время на старте программы, мы теперь будем терять его каждый раз при обращении к Singleton’у из-за того, что метод синхронизирован, а это очень дорого, если к экземпляру приходится часто обращаться. А ведь единственный раз, когда свойство synchronised действительно требуется — первое обращение к методу.

Есть два способа решить эту проблему. Первый — пометить как synchronised не весь метод, а только блок, где создаётся объект:

Не забывайте, что это нельзя использовать в версии Java ниже, чем 1.5, потому что там используется иная модель памяти. Также не забудьте пометить поле instance как volatile .

Второй путь — использовать паттерн «Lazy Initialization Holder». Это решение основано на том, что вложенные классы не инициализируются до первого их использования (как раз то, что нам нужно):

Рефлексия

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

Конечно, если вы используете Singleton только в своём приложении, переживать не о чем. А вот если вы разрабатываете модуль, который затем будет использоваться в сторонних приложениях, то из-за этого могут возникнуть проблемы. Какие именно, зависит от того, что делает ваш «Одиночка» — это могут быть как и риски, связанные с безопасностью, так и просто непредсказуемое поведение модуля.

Заключение

Несмотря на то, что паттерн Singleton очень известный и популярный, у него есть множество серьёзных недостатков. Чем дальше, тем больше этих недостатков выявляется, и оригинальные паттерны из книги GOF «Design Patterns» часто сегодня считаются антипаттернами. Тем не менее, сама идея иметь лишь один объект на класс по-прежнему имеет смысл, но достаточно сложно реализовать ее правильно.

Что такое антипаттерны? Разбираем примеры (часть 1)

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

Синглтон нарушает один из принципов SOLID — Single Responsibility Principle — класс синглтона, помимо выполнения своих непосредственных обязанностей, занимается еще и контролированием количества своих экземпляров.

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

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

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

И если задуматься, использования синглтона можно избежать. Например, для контроля количества экземпляров объекта вполне можно (да и нужно) использовать различного рода фабрики.

Наибольшая же опасность подстерегает при попытке построить на основе синглтонов всю архитектуру приложения. Такому подходу существует масса замечательных альтернатив. Самый главный пример — это Spring, а именно — его IoC контейнеры: там проблема контроля создания сервисов решается естественным образом, так как они, по факту, являются «фабриками на стероидах».

Сейчас существует много много холивара на эту тему, ну и решать, синглтон — это паттерн или антипаттерн, уже вам.

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

4. Poltergeist

5. Hard code

6. Boat anchor

Что такое антипаттерны? Разбираем примеры (часть 1) - 4

Лодочный якорь в контексте антипаттернов означает хранение неиспользуемых частей системы, которые остались после какой-то оптимизации или рефакторинга. Также некоторые части кода могли быть оставлены «на будущее», вдруг придётся ещё их использовать. По сути это делает из кода мусорное ведро. Пример: У нас есть метод обновления, который использует отдельный метод для слияния данных пользователя из базы данных и пришедшего на обновление (если у пришедшего на обновление пустое поле, то оно записывается старым из базы данных). И к примеру, появилось требование, что записи не должны объединяться со старыми, а перезаписываться поверх, даже если есть пустые поля: Как итог, mergeUser уже не используется и жалко его удалять: вдруг он (или его идея) ещё пригодится? Такой код только усложняет и путает системы, по сути не неся вовсе никакой практической ценности. Нужно не забывать, что подобный код с «мертвыми кусками» будет тяжело передавать коллеге, когда вы уйдете на другой проект. Лучшим методом борьбы с лодочными якорями является рефакторинг кода, именно — удаление данных участков кода (увы, увы). Также при планировании разработки нужно учитывать возникновение подобных якорей (выделять время на зачистку хвостов).

7.Object cesspool

Для описания данного антипаттерна для начала нужно познакомиться с паттерном пул объектов . Пул объектов (пул ресурсов) — порождающий шаблон проектирования, набор инициализированных и готовых к использованию объектов. Когда приложению требуется объект, он не создается заново, а берётся из этого пула. Когда объект уже не нужен, он не уничтожается, а возвращается в пул. Обычно используется для тяжёлых объектов, которые ресурсозатратно каждый раз создавать, как например соединение с базой данных. Давайте разберем небольшой и простой пример ради примера. Итак, у нас есть класс, представляющий данный паттерн: Данный класс у нас представлен в виде вышеописанного паттерна/антипаттерна синглтона , то есть может быть только один объект данного типа, оперирует некими объектами Resource , по умолчанию в конструкторе пул заполняется 4-мя экземплярами; при взятии такого объекта он удаляется из пула (если его нет, то создается и сразу отдается), и в конце — метод, чтобы положить объект обратно. Объекты Resource выглядят так: Тут у нас небольшой объект, содержащий мапу с названиями паттернов в качестве ключа и ссылками на них как значение, а также методов доступа к мапе. Смотрим main : Тут всё тоже понятно: мы берём объект пула, вытягиваем из него объект с ресурсами, берём из него мапу, что-то с ней делаем и кладем всё это на место в пул для дальнейшего переиспользования. Вуаля: вот вам и паттерн пул объектов. Но мы же говорили об антипатернах, не так ли? Давайте рассмотрим такой случай в main : Тут опять же берется объект ресурсов, берётся его map с паттернами и что-то с ним делается, но перед сохранением назад в пул объектов мапа чистится и забивается непонятными данными, делающими данный объект Resource непригодным для переиспользования. Один из главных нюансов пула объектов — после того, как объект возвращен, он должен вернуться в состояние, пригодное для дальнейшего переиспользования. Если объекты после возвращения в пул оказываются в неправильном или неопределенном состоянии, такая конструкция и называется объектной клоакой. Что такое антипаттерны? Разбираем примеры (часть 1) - 5Смысл нам хранить объекты, непригодные для переиспользования? В данной ситуации можно сделать в конструкторе внутреннюю мапу неизменяемой: (попытки и желание поменять содержимое упадут вместе с UnsupportedOperationException). Антипаттерны — ловушки, в которые разработчик часто встревает из-за острой нехватки времени, невнимательности, неопытности или пинков со стороны менеджеров. Обычная нехватка времени и спешка может вылиться в большие проблемы для приложения в будущем, поэтому эти ошибки нужно нужно знать и заранее их избегать. Что такое антипаттерны? Разбираем примеры (часть 1) - 6На этом, первая часть статьи подошла концу: продолжение следует.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *