Паттерн Одиночка
Паттерн «Одиночка» (Singleton) является одним из паттернов проектирования, который используется для создания класса, имеющего только один экземпляр в системе, и предоставляющего глобальную точку доступа к этому экземпляру. Это означает, что в рамках приложения может существовать только один объект данного класса, и любой запрос на создание нового экземпляра будет возвращать ссылку на существующий.
Паттерн обеспечивает механизм глобального доступа к единственному экземпляру класса, что упрощает взаимодействие с этим объектом из любой части приложения.
Паттерн может быть реализован разными способами, включая ленивую инициализацию (создание экземпляра при первом запросе), мгновенную инициализацию (создание экземпляра при загрузке класса), и использование синхронизации для обеспечения потокобезопасности.
Основные принципы паттерна
Паттерн гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру.
Создадим простой Singleton класс и рассмотрим его основные элементы:
class Singleton: _instance = None # Приватное поле для хранения единственного экземпляра def __new__(cls): if cls._instance is None: cls._instance = super(Singleton, cls).__new__(cls) return cls._instance
Коротко про каждую строчку (более подробно о каждой будет ниже):
- class Singleton: — Это определение класса Singleton.
- _instance = None — Это приватное поле класса, которое будет хранить единственный экземпляр класса. Изначально оно устанавливается в None , что означает, что экземпляр еще не создан.
- def __new__(cls): — Это метод __new__ , который переопределяется в классе Singleton. Метод __new__ вызывается при создании нового объекта. Мы переопределяем его, чтобы контролировать создание экземпляров.
- if cls._instance is None: — Это проверка наличия существующего экземпляра класса. Если _instance равно None , это означает, что экземпляр еще не создан, и мы можем создать новый экземпляр.
- cls._instance = super(Singleton, cls).__new__(cls) — Здесь мы создаем новый экземпляр класса, если он еще не существует. Мы используем функцию super() для вызова базовой реализации метода __new__ , которая фактически создает новый экземпляр класса.
- return cls._instance — Мы возвращаем существующий или только что созданный экземпляр класса. Это дает уверенность в том, что всегда будет использоваться только один экземпляр класса.
Уникальность экземпляря
Когда у вас есть только один экземпляр класса, вы можете предоставить глобальную точку доступа к этому объекту. Это означает, что любая часть вашего приложения может обратиться к этому экземпляру, не зная о его создании или управлении.
В некоторых случаях создание экземпляров определенных классов может быть ресурсоемким или даже нежелательным. Например, если у вас есть класс, представляющий соединение с базой данных, создание множества экземпляров этого класса может негативно сказаться на производительности и использовании ресурсов.
Реализация уникальности экземпляра
Для гарантированного существования только одного экземпляра класса конструктор этого класса должен быть сделан приватным. Это означает, что нельзя будет создать экземпляр класса извне.
Чтобы получить доступ к единственному экземпляру класса, обычно создается статический метод (например, getInstance() ), который контролирует создание и возврат экземпляра класса. Внутри этого метода происходит проверка: если экземпляр уже существует, то он возвращается; если нет, то создается новый экземпляр.
Можно выбрать, когда именно создавать экземпляр класса. Ленивая инициализация означает создание объекта только при первом запросе, что может быть полезно, если создание объекта дорогостоимо. Мгновенная инициализация подразумевает создание объекта сразу при загрузке класса, что может быть полезно, если объект всегда требуется сразу.
Если несколько потоков попытаются создать экземпляр одновременно, это может привести к созданию нескольких экземпляров. Для обеспечения потокобезопасности можно использовать синхронизацию или дабл чек при создании экземпляра.
Глобальная точка доступа
В первую очередь, чтобы обеспечить глобальную точку доступа к объекту, конструктор класса Singleton делается приватным (private). Это означает, что нельзя будет создать экземпляр класса извне класса.
Для получения доступа к этому единственному экземпляру класса, создается статический метод (например, getInstance() ). Этот метод контролирует создание и возврат экземпляра класса:
public class Singleton < private static Singleton instance; private Singleton() <>public static Singleton getInstance() < if (instance == null) < instance = new Singleton(); >return instance; > >
Метод getInstance() содержит логику для создания объекта Singleton при первом обращении. Это называется ленивой инициализацией (о ней чуть позже). Это означает, что экземпляр класса будет создан только тогда, когда он действительно понадобится.
Простой подход к синхронизации метода getInstance() может выглядеть так:
public class Singleton < private static Singleton instance; private Singleton() <>public static synchronized Singleton getInstance() < if (instance == null) < instance = new Singleton(); >return instance; > >
Код использует ключевое слово synchronized , чтобы обеспечить атомарную и потокобезопасную инициализацию Singleton. Однако синхронизация может снижать производительность, и существуют более эффективные способы обеспечения потокобезопасности.
Когда глобальная точка доступа реализована, любая часть вашего приложения может получить доступ к экземпляру Singleton, вызывая метод getInstance() :
Singleton singleton = Singleton.getInstance();
Этот объект Singleton можно использовать для хранения глобальных настроек, управления ресурсами или предоставления служб и функциональности, доступной из любой точки приложения.
Ленивая инициализация — это важный аспект реализации паттерна «Одиночка» (Singleton) с технической точки зрения. Этот принцип гарантирует, что экземпляр класса Singleton будет создан только при первом запросе на него, что может быть весьма полезным для оптимизации ресурсов и ускорения инициализации вашего приложения.
Ленивая инициализация
Для создания единственного экземпляра класса и предоставления доступа к нему, создается статический метод (например, getInstance() ). Этот метод будет ответственным за создание экземпляра класса, но только при его первом вызове:
public class Singleton < private static Singleton instance; private Singleton() <>public static Singleton getInstance() < if (instance == null) < instance = new Singleton(); //создание экземпляра при первом обращении >return instance; > >
В методе getInstance() добавляется проверка: если экземпляр еще не создан ( instance == null ), то он создается. Важно заметить, что при последующих вызовах getInstance() , уже существующий экземпляр будет возвращен, и новый экземпляр не будет создаваться.
тот подход позволяет избегать создания экземпляра класса, если он не используется. Это мастхев, если создание объекта требует значительных ресурсов (например, соединение с базой данных или загрузка больших данных).
Приложение будет инициализироватьс быстрее, так как экземпляр Singleton создается только при реальной необходимости, а не при запуске программы.
Обработка многопоточности
Если несколько потоков одновременно попытаются создать экземпляр синглтона в условиях ленивой инициализации, это может привести к созданию нескольких экземпляров.
Если несколько потоков попытаются одновременно внести изменения в экземпляр синглтона, это может привести к состоянию объекта, которое не соответствует ожиданиям.
Самый простой способ обеспечить потокобезопасность — использовать ключевое слово synchronized , мы его использовали чуть выше. Это означает, что только один поток может одновременно выполнять код, защищенный синхронизацией.
public class Singleton < private static Singleton instance; private Singleton() <>public static synchronized Singleton getInstance() < if (instance == null) < instance = new Singleton(); >return instance; > >
Однако синхронизация может снижать производительность.
Double-Checked Locking»(DCL): этот подход позволяет избежать синхронизации при каждом вызове getInstance() . Он предполагает двойную проверку: сначала проверка без синхронизации, а затем с синхронизацией, только если экземпляр не был создан. Этот метод более эффективен:
public class Singleton < private static volatile Singleton instance; // Волатильность для корректной работы DCL private Singleton() <>public static Singleton getInstance() < if (instance == null) < synchronized (Singleton.class) < if (instance == null) < instance = new Singleton(); >> > return instance; > >
Некоторые япы предоставляют встроенные механизмы для обеспечения безопасности в многопоточной среде. В джаве, например, можно использовать класс java.util.concurrent.atomic.AtomicReference для ленивой инициализации синглтона:
public class Singleton < private static final AtomicReferenceinstance = new AtomicReference<>(); private Singleton() <> public static Singleton getInstance() < if (instance.get() == null) < instance.compareAndSet(null, new Singleton()); >return instance.get(); > >
Сценарии, когда стоит использовать альтернативные подходы
Думаю, что всем понятно, что паттерны имееют свои минусы. И возможно, какие то минусы в паттерне нашего дня вы уже заметили.
Паттерн «одиночка» полезен во многих случаях, но существуют сценарии, когда стоит рассмотреть алтернативу:
- Изменение глобального состояния: Если ваше приложение изменяет глобальное состояние слишком часто или слишком интенсивно, это может сделать код сложным и трудноподдерживаемым. Вместо использования Singleton для хранения глобального состояния, можно рассмотреть использование инъекции зависимостей (Dependency Injection) и передачу состояния через параметры функций и методов. Сделает код более предсказуемым.
- Множественные экземпляры для тестирования: В некоторых случаях для тестирования требуется иметь несколько экземпляров объекта, чтобы изолировать и провести модульное тестирование. Вместо использования Singleton в коде, можно создавать экземпляры классов с зависимостями в тестовом окружении. Для тестирования могут также использоваться моки (mock objects) или фейковые объекты (fake objects).
- Недостаток потокобезопасности: Если ваша реализация Singleton не обеспечивает потокобезопасность и вы сталкиваетесь с проблемами с многопоточностью. Можно использовать более сложные механизмы для обеспечения потокобезопасности, такие как блокировки (locks) или использование классов из библиотеки threading . Кстати, можно еще использовать пул объектов (object pooling) для ресурсоемких операций.
- Сложная иерархия зависимостей: Если ваш класс Singleton имеет сложную иерархию зависимостей с другими классами, что делает его создание и управление сложным. Здесь более уместно применение паттернов внедрения зависимостей и инверсии управления. Еще Dagger в Python, может значительно упростить управление зависимостями и сделать код более гибким.
- Ленивая инициализация не требуется: Если создание экземпляра класса не требует значительных ресурсов и не влияет на производительность приложения. В этом случае можно просто создавать экземпляры класса по мере необходимости, без использования Singleton. Это уменьшит сложность кода и избежит излишней оптимизации.
Заключение
Существование только одного экземпляра класса одиночки и обеспечивает глобальную точку доступа к этому экземпляру. Правильное применение паттерна «Одиночка» зависит от специфики вашего проекта и его требований. Умело использованный Singleton может значительно улучшить структуру вашего app.
Разработка ПО является сложным и многогранным процессом, и каждый подход имеет свои сильные и слабые стороны. По устоявшейся традиции хочу порекомендовать вам бесплатный вебинар, на котором вы познакомитесь с несколькими основными методологиями разработки ПО, такими как водопадная модель, итеративная разработка, спиральная модель, гибкая разработка и другие. Эксперты OTUS рассмотрят каждый подход в отдельности и объяснят, в каких ситуациях их использование может быть наиболее эффективным. Регистрация доступна по ссылке.
- Блог компании OTUS
- Программирование
Шаблон проектирования Singleton
Шаблон проектирования Singleton также называют шаблоном проектирования «Одиночка». Это порождающий шаблон, который гарантирует, что в однопроцессном программном приложении будет лишь один экземпляр некого класса. Также шаблон предоставляет глобальную точку доступа к вышеупомянутому единственному экземпляру.
Если абстрагироваться от этого скучного термина, взятого из Википедии, можно привести понятный, но немного грубый пример из реальной жизни: в стране может быть лишь один президент, этот президент действует по обстоятельствам и, в каком-то смысле, этот президент и является одиночкой. Скажем так, шаблон обеспечивает, что создаваемый объект — это единственный объект своего класса.
Прежде чем продолжить, скажем, что этот шаблон не стоит чрезмерно использовать. Все дело в том, что Singleton признан антипаттерном, однако это не значит, что он обязательно всегда плох. Порой, он может быть полезным, однако применять его следует с осторожностью. Он вводит в ваше приложение глобальное состояние, в результате чего его изменение в одном месте способно повлиять и на другие части разрабатываемого приложения, а это уже может вызвать трудности процесса отладки. И второй минус — Singleton делает ваш код связанным, то есть подводные камни-таки имеются.
Но давайте лучше перейдем к коду. Для создания «Одиночки» следует сделать конструктор приватным, отключить клонирование и расширение, плюс создать статическую переменную, необходимую для хранения экземпляра:
А вот и пример использования:
Если интересует, есть реализация и на Java.
Singleton (Одиночка)
Допустим, мы решили выражать арифметические операции объектами.
public final class Sum implements IntBinaryOperator < @Override public int applyAsInt(int left, int right) < return left + right; >>
class Sum : (Int, Int) -> Int
new Sum().applyAsInt(42, 24);
Sum()(42, 24)
У этого класса есть важное свойство: все его экземпляры одинаковы. Превратим его в синглтон — пусть существует только один его экземпляр:
public final class Sum implements IntBinaryOperator < private static final Sum INSTANCE = new Sum(); public static Sum getInstance() < return INSTANCE; >private Sum() < // приватный конструктор // запрещает создание извне >@Override public int applyAsInt(int left, int right) < return left + right; >>
object Sum : (Int, Int) -> Int
Sum.getInstance().applyAsInt(42, 24);
Sum(42, 24)
Такая маленькая хитрость идёт на пользу производительности, но есть несколько способов всё испортить.
Антипаттерн: синглтон с зависимостями
Напишем то же самое для Byte , опираясь на существующую реализацию Sum для int .
public final class ByteSum implements BinaryOperator < private static final ByteSum INSTANCE = new ByteSum(); public static ByteSum getInstance() < return INSTANCE; >private ByteSum() <> @Override public Byte apply(Byte left, Byte right) < return (byte) Sum.getInstance().applyAsInt(left, right); >>
object ByteSum : (Byte, Byte) -> Byte
Здесь у ByteSum есть зависимость — Sum . Таким образом, для другой операции (например, Diff ), пусть и реализующей тот же интерфейс, придётся писать другую обёртку ( ByteDiff ).
По-хорошему, класс не должен знать о своих зависимостях, они должны передаваться через конструктор. Исправляем:
public final class IntOperatorByteAdapter implements BinaryOperator < private final IntBinaryOperator operator; public IntOperatorByteAdapter(IntBinaryOperator operator) < this.operator = operator; >@Override public Byte apply(Byte left, Byte right) < return (byte) operator.applyAsInt(left, right); >>
class IntOperatorByteAdapter( private val intOperator: (Int, Int) -> Int ) : (Byte, Byte) -> Byte
Теперь класс не разрешает свои зависимости самостоятельно, они передаются в конструктор. Соответственно, вместо ByteSum , ByteDiff , ByteMultiplication , ByteDivision будет IntOperatorByteAdapter .
Антипаттерн: синглтон с состоянием
Допустим, нужно считать количество активных соединений:
public final class Application < private static final Application INSTANCE = new Application(); public static Application getInstance() < return INSTANCE; >private Application() < >private int connections = 0; public void connected() < connections++; >public void disconnected() < connections--; >>
object Application < private var connections = 0 fun connected() < connections++ >fun disconnected() < connections-- >>
Соответственно, внутри проекта встречаются такие строки:
Application.getInstance().connected(); try < . >finally
Application.connected() try < . >finally
- Переиспользуемость кода понижается: компоненты, которые используют Application , изменяют глобальное состояние, как бы мы их ни использовали.
- Синглтон невозможно подменить для тестирования. Тесты начинают зависеть от глобального состояния.
- Любой код может использовать синглтон, поэтому сложнее контролировать, из каких потоков он используется и не изменяют ли его из разных потоков одновременно.
Если превратить этот синглтон в нормальный класс, получим что-то такое:
public final class ConnectionCount < private int connections = 0; public void connected() < connections++; >public void disconnected() < connections--; >>
class ConnectionCount < private var connections = 0 fun connected() < connections++ >fun disconnected() < connections-- >>
Далее создадим экземпляр и передадим его тем классам, которым он необходим:
ConnectionCount cc = new ConnectionCount(); new SomeComponent( someDependency, new Something(), cc, . );
val cc = ConnectionCount() SomeComponent( someDependency, Something(), cc, . )
Рассмотрим, применимы ли проблемы синглтона к этому решению.
- Глобального состояния больше нет. Разным компонентам при необходимости можно передать разные экземпляры ConnectionCount .
- При тестировании можно создать локальный экземпляр, доступный только одному тесту.
- При создании объектов передаём ConnectionCount явно, что позволяет проще определить, используется ли он из разных потоков, и внести соответствующие поправки.
Антипаттерн: ленивая инициализация синглтона
Сама по себе ленивая инициализация, т. е. инициализация при первом обращении — хороший шаблон. Рассмотрим его в контексте синглтонов.
public final class Sloth < private static Sloth instance; private Sloth() <>public static Sloth getInstance() < if (instance == null) < instance = new Sloth(); >return instance; > >
class Sloth private constructor() < // . private companion object < val instance: Sloth get() < if (_instance == null) < _instance = Sloth() >return _instance!! > private var _instance: Sloth? = null > >
Проблема первая: этот код не потокобезопасен. Если из разных потоков запросить instance одновременно, можно увидеть разные значения. Исправляем:
public synchronized Sloth getInstance()
@Synchronized fun getInstance(): Sloth
Теперь появляется другая проблема: блокировка захватывается при каждом вызове getInstance , даже если instance давно инициализирован. Применим double-checking, чтобы захватывать блокировку только при инициализации.
public static Sloth getInstance() < if (instance == null) < synchronized (Sloth.class) < if (instance == null) < Sloth sloth = new Sloth(); instance = sloth; return sloth; >> > return instance; >
val instance: Sloth get() < if (_instance == null) < synchronized(this) < if (_instance == null) < val sloth = Sloth() _instance = sloth return sloth >> > return _instance!! >
Теперь у нас есть уродливый, зато безопасный (если честно, не совсем) код.
Классы в JVM (и в Android) загружаются лениво, т. е. по первому требованию. При загрузке класса виртуальная машина самостоятельно удерживает блокировку, поэтому даже в условии гонки, когда несколько потоков требуют загрузки одного класса, инициализатор класса выполнится ровно один раз. Это значит, что «ручная» ленивая инициализация синглтона бесполезна.
Правило
У синглтона нет ни зависимостей, ни состояния, в противном случае он не должен быть синглтоном. Ленивую и безопасную инициализацию обеспечивает загрузчик классов.
Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью
Паттерн “Одиночка” — пожалуй, самый известный паттерн проектирования. Тем не менее, он не лишен недостатков, поэтому некоторые программисты (например, Егор Бугаенко) считают его антипаттерном. Разбираемся в том, какие же подводные камни таятся в Singleton’е.
Определение паттерна
Само описание паттерна достаточно простое — класс должен гарантированно иметь лишь один объект, и к этому объекту должен быть предоставлен глобальный доступ. Скорее всего, причина его популярности как раз и кроется в этой простоте — всего лишь один класс, ничего сложного. Это, наверное, самый простой для изучения и реализации паттерн. Если вы встретите человека, который только что узнал о существовании паттернов проектирования, можете быть уверены, что он уже знает про Singleton. Проблема заключается в том, что когда из инструментов у вас есть только молоток, всё вокруг выглядит как гвозди. Из-за этого “Одиночкой” часто злоупотребляют.
Простейшая реализация
Как уже говорилось выше, в этом нет ничего сложного:
- Сделайте конструктор класса приватным, чтобы не было возможности создать экземпляр класса извне.
- Храните экземпляр класса в private static поле.
- Предоставьте метод, который будет давать доступ к этому объекту.
public class Singleton < private static Singleton instance = new Singleton(); private Singleton() < >public static Singleton getInstance() < return instance; >>
Принцип единственной обязанности
В объектно-ориентированном программировании существует правило хорошего тона — “Принцип едиственной обязанности” (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 содержит ленивую инициализацию. Это значит, что объект класса создаётся не в самом начале, а лишь когда будет получено первое обращение к нему. Добиться этого совсем не сложно:
public static Singleton getInstance() < if (instance == null) < instance = new Singleton(); >return instance; >
Однако здесь начинаются проблемы с потоками, которые могут создавать несколько различных объектов. Происходит это примерно так:
- Первый поток обращается к getInstance() , когда объект ещё не создан;
- В это время второй тоже обращается к этому методу, пока первый ещё не успел создать объект, и сам создаёт его;
- Первый поток создаёт ещё один, второй, экземпляр класса.
Разумеется, можно просто пометить метод как synchronised , и эта проблема исчезнет. Проблема заключается в том, что, сохраняя время на старте программы, мы теперь будем терять его каждый раз при обращении к Singleton’у из-за того, что метод синхронизирован, а это очень дорого, если к экземпляру приходится часто обращаться. А ведь единственный раз, когда свойство synchronised действительно требуется — первое обращение к методу.
Есть два способа решить эту проблему. Первый — пометить как synchronised не весь метод, а только блок, где создаётся объект:
public static Singleton getInstance() < if (instance == null) < synchronized (Singleton.class) < if (instance == null) < instance = new Singleton(); >> > return instance; >
Не забывайте, что это нельзя использовать в версии Java ниже, чем 1.5, потому что там используется иная модель памяти. Также не забудьте пометить поле instance как volatile .
Второй путь — использовать паттерн “Lazy Initialization Holder”. Это решение основано на том, что вложенные классы не инициализируются до первого их использования (как раз то, что нам нужно):
public class Singleton < private Singleton() < >public static Singleton getInstance() < return SingletonHolder.instance; >private static class SingletonHolder < private static final Singleton instance = new Singleton(); >>
Рефлексия
Мы запрещаем создавать несколько экземпляров класса, помечая конструктор приватным. Тем не менее, используя рефлексию, можно без особого труда изменить видимость конструктора с private на public прямо во время исполнения:
Class clazz = Singleton.class; Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true);
Конечно, если вы используете Singleton только в своём приложении, переживать не о чем. А вот если вы разрабатываете модуль, который затем будет использоваться в сторонних приложениях, то из-за этого могут возникнуть проблемы. Какие именно, зависит от того, что делает ваш “Одиночка” — это могут быть как и риски, связанные с безопасностью, так и просто непредсказуемое поведение модуля.
Заключение
Несмотря на то, что паттерн Singleton очень известный и популярный, у него есть множество серьёзных недостатков. Чем дальше, тем больше этих недостатков выявляется, и оригинальные паттерны из книги GOF “Design Patterns” часто сегодня считаются антипаттернами. Тем не менее, сама идея иметь лишь один объект на класс по-прежнему имеет смысл, но достаточно сложно реализовать ее правильно.