tl; dr : хотя я думаю, что можно создать синглтон таким образом, чтобы его можно было разделить на подклассы, класс должен быть разработан с особой тщательностью. В большинстве случаев я думаю, что это не стоит усилий.
Я бы не сказал, что создание подклассов является неотъемлемым нарушением принципа синглтона, но при разработке такого класса необходимо соблюдать особую осторожность. Чтобы понять почему, давайте взглянем на противоположные определения в этой проблеме.
Первое определение - это Singleton Pattern . По сути, это говорит о том, что для этого класса будет не более одного экземпляра этого класса, и все классы используют этот же экземпляр. В Java это обычно означает, что синглтон не может быть повторно инициализирован (т. Е. Должен быть предотвращен второй вызов new ...
). Зачем? Потому что где-то в вашей программе какая-то часть может содержать ссылку x
на «старый» экземпляр синглтона. Когда вы повторно инициализируете синглтон, x
все равно будет ссылаться на старый синглтон, в то время как все будущие вызовы метода доступа синглтона будут содержать ссылку на новый синглтон. У вас есть два экземпляра одного класса, поэтому вы нарушаете синглтон.
Определение против единственного wrt. подкласс - это законы о замене Лискова . По сути, в нем говорится, что каждый подкласс класса X
должен использоваться в месте X
, и программа все еще должна работать. Вот почему Square
и Rectangle
никогда не должны стоять - это отношение наследования (одному нужно определить только одно свойство [length
], а другому - два [width
& height
]).
Это означает, что для области действия синглтона все экземпляры подкласса также считаются экземплярами суперкласса. Вы должны не только управлять созданием рассматриваемого класса, но также и всех (!) Его подклассов, даже тех, которые не находятся под вашим контролем. В худшем случае вы даже не знаете всех классов, которые подкласс вашего синглтона.
Я кодировал небольшой пример, который охватывает случаи, которые я вижу. Обратите внимание, что этот пример не гарантированно будет идеальным, он просто показывает, насколько вам нужно продвинуться, чтобы правильно создать подкласс Singleton
.
class Singleton {
private static volatile Singleton INSTANCE = null;
private static Supplier<? extends Singleton> factory;
public static void setSingletonFactory(Supplier<? extends Singleton> factory) {
Singleton.factory = factory;
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = factory.get();
}
}
}
return INSTANCE;
}
protected Singleton() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
// Set attribute of Singleton as necessary
} else {
throw cannotReinitializeSingletonIllegalStateException();
}
}
} else {
throw cannotReinitializeSingletonIllegalStateException();
}
}
private static IllegalStateException cannotReinitializeSingletonIllegalStateException() {
return new IllegalStateException("Cannot reinitialize Singleton");
}
public static void main(String... args) {
Singleton.setSingletonFactory(SubSingleton.SUB_SINGLETON_FACTORY);
Singleton instanceOne = Singleton.getInstance();
Singleton instanceTwo = Singleton.getInstance();
System.out.println(instanceOne == instanceTwo);
try {
SubSingleton.SUB_SINGLETON_FACTORY.get();
} catch (IllegalStateException e) {
System.out.println("Rightfully thrown IllegalStateException:");
e.printStackTrace(System.out);
}
}
}
class SubSingleton extends Singleton {
public static final Supplier<SubSingleton> SUB_SINGLETON_FACTORY = SubSingleton::new;
private SubSingleton() {}
}
Требуется двойная проверка блокировки в двух экземплярах (getInstance()
и конструктор), если вызов getInstance()
критичен к производительности, т. Е. Вы не можете позволить себе блокировать каждый вызов для него. Кроме того, если синглтон используется по назначению, вызов getInstance()
никогда не должен сбрасываться. В конструкторе нам нужна блокировка с двойной проверкой, чтобы применить свойство singleton. В getInstance()
он нужен нам для предотвращения преждевременного выброса IllegalStateException
s.
Видя приведенное выше доказательство концепции, становится очевидным, что необходимо проявить определенную осторожность. Существует вероятность того, что где-то в вашей программе экземпляр подкласса будет создан до того, как будет выполнен первый вызов getInstance()
. Это будет означать, что любой вызов getInstance()
вызовет IllegalStateException
(вы в основном забросали систему). Насколько я знаю, у нас нет возможности как гибко вводить фабрику, так и предотвратить вызов конструкторов подкасс за пределами предполагаемого пути.