我恭敬地提供与建议单例不应该被子类化的 cmets 的对立面。 Gamma、Helm、Johnson 和 Vlissides 的“Design Patterns: Elements of Reusable Object-Oriented Software”(又名“The Gang of Four”一书或简称“GOF”)中讨论了单例的子类化。
一个正确子类化的单例也将是一个单例。例如,假设您有一个单例来处理名为 Logger 的记录信息性消息。现在假设您想扩展 Logger 的功能以使用 HTML 标记写入输出。我们称它为 HTMLLogger。这两个类都是单例类,但其中一个扩展了另一个的功能。
首先,这是一个简单的单例及其测试用例:
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.simple;
public class SimpleSingleton {
// The instance - only one of these can exist in the system (currently not accounting for threads).
private static SimpleSingleton instance;
private int sampleValue;
public static SimpleSingleton getInstance() {
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
public int getSampleValue() {
return sampleValue;
}
public void setSampleValue(int sampleValue) {
this.sampleValue = sampleValue;
}
protected SimpleSingleton() {
// Insures construction cannot occur outside of class.
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.simple.test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import study.design.patterns.creational.singleton.simple.SimpleSingleton;
public class SimpleSingletonTest {
@Test
public void testIllegalCreation() {
// The following line will not compile because the constructor is not visible.
// Singleton instance = new Singleton();
}
@Test
public void testObjectEquality() {
SimpleSingleton instance1 = SimpleSingleton.getInstance();
assertNotNull(instance1);
SimpleSingleton instance2 = SimpleSingleton.getInstance();
assertNotNull(instance2);
assertEquals(instance1, instance2);
}
@Test
public void testDataEquality() {
SimpleSingleton instance1 = SimpleSingleton.getInstance();
assertNotNull(instance1);
SimpleSingleton instance2 = SimpleSingleton.getInstance();
assertNotNull(instance2);
assertEquals(instance1, instance2);
instance1.setSampleValue(5);
int testSampleValue = instance2.getSampleValue();
assertEquals(testSampleValue, 5);
}
}
/////////////////////////////////////////////////////////////////////////////
我发现了四种方法可以将单例子类化。
选项 1. 蛮力。
本质上,子类重新实现了使类成为单例的关键特性。即静态实例变量、静态getInstance方法和隐藏构造函数。在这种情况下,隐藏的构造函数调用基类。
这是一个示例基类、子类和测试用例:
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassbruteforce;
// This singleton can be extended (subclassed)
public class BruteForceExtendableSingleton {
// The instance - only one of these can exist in the system (currently not accounting for threads).
private static BruteForceExtendableSingleton instance;
private int sampleValue;
public static BruteForceExtendableSingleton getInstance() {
// The problem with this version of an extendable singleton is clear from the code below - every subclass possible is hard-coded.
// Creating a new subclass requires modifying the base class as well, which violates the open-closed principle.
if (instance == null) {
instance = new BruteForceExtendableSingleton();
}
return instance;
}
public int getSampleValue() {
return sampleValue;
}
public void setSampleValue(int sampleValue) {
this.sampleValue = sampleValue;
}
protected BruteForceExtendableSingleton() {
// Insures construction cannot occur outside of class.
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassbruteforce;
public class BruteForceSubclassSingleton extends BruteForceExtendableSingleton {
// The instance - only one of these can exist in the system (currently not accounting for threads).
private static BruteForceSubclassSingleton instance;
private int sampleValue2;
public static BruteForceSubclassSingleton getInstance() {
// The problem with this version of an extendable singleton is clear from the code below - every subclass possible is hard-coded.
// Creating a new subclass requires modifying the base class as well, which violates the open-closed principle.
if (instance == null) {
instance = new BruteForceSubclassSingleton();
}
return instance;
}
public int getSampleValue2() {
return sampleValue2;
}
public void setSampleValue2(int sampleValue2) {
this.sampleValue2 = sampleValue2;
}
protected BruteForceSubclassSingleton() {
super();
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassbruteforce.test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import study.design.patterns.creational.singleton.subclassbruteforce.BruteForceExtendableSingleton;
import study.design.patterns.creational.singleton.subclassbruteforce.BruteForceSubclassSingleton;
public class BruteForceExtendableSingletonTest {
@Test
public void testIllegalCreation() {
// The following lines will not compile because the constructor is not visible.
// BruteForceExtendableSingleton instance = new BruteForceExtendableSingleton();
// BruteForceSubclassSingleton instance2 = new BruteForceSubclassSingleton();
}
@Test
public void testCreateBruteForceExtendableSingleton() {
BruteForceExtendableSingleton singleton = BruteForceExtendableSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is an ExtendableSingleton, but not a FixedSubclassSingleton.
assertTrue(singleton instanceof BruteForceExtendableSingleton);
assertFalse(singleton instanceof BruteForceSubclassSingleton);
}
@Test
public void testCreateBruteForceSubclassSingleton() {
BruteForceExtendableSingleton singleton = BruteForceSubclassSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is a BruteForceSubclassSingleton.
assertTrue(singleton instanceof BruteForceSubclassSingleton);
}
@Test
public void testCreateBothBruteForceSingletons() {
BruteForceExtendableSingleton singleton = BruteForceExtendableSingleton.getInstance();
assertNotNull(singleton);
assertTrue(singleton instanceof BruteForceExtendableSingleton);
assertFalse(singleton instanceof BruteForceSubclassSingleton);
BruteForceExtendableSingleton singleton2 = BruteForceSubclassSingleton.getInstance();
assertNotNull(singleton2);
assertTrue(singleton2 instanceof BruteForceSubclassSingleton);
assertFalse(singleton == singleton2);
}
}
/////////////////////////////////////////////////////////////////////////////
优点:
允许两个单例同时存在。
缺点:
重复创建单例的工作。子类的单例性质并非来自其基类。
如果单例需要分开,可能需要更好的设计来共享其他方法而不是子类化。
选项 2。从一组固定的类中进行选择。
在这种情况下,基类中的 getInstance 方法根据标志(例如系统属性)确定要使用的实例。在代码示例中,我使用了类本身的名称。使用一系列 if 块,代码决定如何初始化实例。
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassfixed;
// This singleton can be extended (subclassed)
public class FixedExtendableSingleton {
// The instance - only one of these can exist in the system (currently not accounting for threads).
private static FixedExtendableSingleton instance;
private int sampleValue;
public static FixedExtendableSingleton getInstance() {
// The problem with this version of an extendable singleton is clear from the code below - every subclass possible is hard-coded.
// Creating a new subclass requires modifying the base class as well, which violates the open-closed principle.
if (instance == null) {
String singletonName = System.getProperty("study.design.patterns.creational.singleton.classname");
if (singletonName.equals(FixedExtendableSingleton.class.getSimpleName())) {
instance = new FixedExtendableSingleton();
} else if (singletonName.equals(FixedSubclassSingleton.class.getSimpleName())) {
instance = new FixedSubclassSingleton();
}
}
return instance;
}
public static void clearInstance() {
// This method wipes out the singleton.
// This is purely for testing purposes so getInstance can reconnect to a new singleton if needed.
instance = null;
}
public int getSampleValue() {
return sampleValue;
}
public void setSampleValue(int sampleValue) {
this.sampleValue = sampleValue;
}
protected FixedExtendableSingleton() {
// Insures construction cannot occur outside of class.
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassfixed;
public class FixedSubclassSingleton extends FixedExtendableSingleton {
private int sampleValue2;
public int getSampleValue2() {
return sampleValue2;
}
public void setSampleValue2(int sampleValue2) {
this.sampleValue2 = sampleValue2;
}
// Must be defined to prevent creation of a public default constructor.
protected FixedSubclassSingleton() {
super();
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassfixed.test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import study.design.patterns.creational.singleton.subclassfixed.FixedExtendableSingleton;
import study.design.patterns.creational.singleton.subclassfixed.FixedSubclassSingleton;
public class FixedExtendableSingletonTest {
@Test
public void testIllegalCreation() {
// The following lines will not compile because the constructor is not visible.
// ExtendableSingleton instance = new ExtendableSingleton();
// FixedSubclassSingleton instance = new FixedSubclassSingleton();
}
@Test
public void testCreateExtendableSingleton() {
System.setProperty("study.design.patterns.creational.singleton.classname", "FixedExtendableSingleton");
FixedExtendableSingleton singleton = FixedExtendableSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is an ExtendableSingleton, but not a FixedSubclassSingleton.
assertTrue(singleton instanceof FixedExtendableSingleton);
assertFalse(singleton instanceof FixedSubclassSingleton);
}
@Test
public void testCreateFixedSubclassSingleton() {
System.setProperty("study.design.patterns.creational.singleton.classname", "FixedSubclassSingleton");
FixedExtendableSingleton singleton = FixedExtendableSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is a FixedSubclassSingleton.
assertTrue(singleton instanceof FixedSubclassSingleton);
}
@AfterEach
protected void tearDown() {
FixedExtendableSingleton.clearInstance();
}
}
/////////////////////////////////////////////////////////////////////////////
优点:
子类与单例行为的更清晰绑定。
减少重复代码。
缺点:
只定义了一组固定的子类。添加新的子类需要修改 getInstance 方法。
选项 3。从一组动态类中确定要使用的单例。
此方法试图消除为每个子类修改 getInstance 的需要。这个想法是在基类中包含一个名称到单例的注册表(映射),并在 getInstance 中查找正确的名称。
为了使用单例填充注册表,需要预先创建每个单例。这是怎么做到的?根据 GOF,我们可以将静态变量分配给对象的实例。加载类时,构造单例,构造函数将对象添加到注册表中。这更复杂,但它有效(有点)。
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassflexible;
import java.util.HashMap;
import java.util.Map;
//This singleton can be extended (subclassed)
public class FlexibleExtendableSingleton {
// The instance - only one of these can exist in the system (currently not accounting for threads).
private static FlexibleExtendableSingleton instance;
// This must appear before thisSingleton, because the constructor requires the registry.
protected static Map<String, FlexibleExtendableSingleton> registry = new HashMap<String, FlexibleExtendableSingleton>();
// This singleton - each class in the hierarchy needs one of these. It will trigger construction (and therefore, registration).
private static FlexibleExtendableSingleton thisSingleton = new FlexibleExtendableSingleton();
public static void activateClass() {
// Do nothing special.
}
private int sampleValue;
protected static void register(String name, FlexibleExtendableSingleton singletonClass) {
registry.put(name, singletonClass);
}
protected static FlexibleExtendableSingleton lookupFromRegistry(String name) {
return registry.get(name);
}
public static FlexibleExtendableSingleton getInstance() {
if (instance == null) {
String singletonName = System.getProperty("study.design.patterns.creational.singleton.classname");
instance = lookupFromRegistry(singletonName);
}
return instance;
}
public static void clearInstance() {
// This method wipes out the singleton.
// This is purely for testing purposes so getInstance can reconnect to a new singleton if needed.
instance = null;
}
public int getSampleValue() {
return sampleValue;
}
public void setSampleValue(int sampleValue) {
this.sampleValue = sampleValue;
}
protected FlexibleExtendableSingleton() {
// Protected insures construction cannot occur outside of class.
// Register the class when it is constructed by its static method.
// Subclasses will be able to use this method as well.
register(this.getClass().getSimpleName(), this);
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassflexible;
import study.design.patterns.creational.singleton.subclassdynamicload.DynamicLoadExtendableSingleton;
public class FlexibleSubclassSingleton extends FlexibleExtendableSingleton {
// This singleton - each class in the hierarchy needs one of these. It will trigger construction (and therefore, registration).
private static FlexibleSubclassSingleton thisSingleton = new FlexibleSubclassSingleton();
private int sampleValue2;
public static void activateClass() {
// Do nothing special.
}
public int getSampleValue2() {
return sampleValue2;
}
public void setSampleValue2(int sampleValue2) {
this.sampleValue2 = sampleValue2;
}
// Must be defined to prevent creation of a public default constructor.
protected FlexibleSubclassSingleton() {
// The following line will also register the class.
super();
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassflexible.test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import study.design.patterns.creational.singleton.subclassflexible.FlexibleExtendableSingleton;
import study.design.patterns.creational.singleton.subclassflexible.FlexibleSubclassSingleton;
public class FlexibleExtendableSingletonTest {
@Test
public void testIllegalCreation() {
// The following lines will not compile because the constructor is not visible.
// FlexibleExtendableSingleton instance = new FlexibleExtendableSingleton();
// FlexibleSubclassSingleton instance2 = new FlexibleSubclassSingleton();
}
@Test
public void testCreateFlexibleExtendableSingleton() {
System.setProperty("study.design.patterns.creational.singleton.classname", "FlexibleExtendableSingleton");
FlexibleExtendableSingleton.activateClass();
FlexibleSubclassSingleton.activateClass();
FlexibleExtendableSingleton singleton = FlexibleExtendableSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is an ExtendableSingleton, but not a FixedSubclassSingleton.
assertTrue(singleton instanceof FlexibleExtendableSingleton);
assertFalse(singleton instanceof FlexibleSubclassSingleton);
}
@Test
public void testCreateFlexibleSubclassSingleton() {
System.setProperty("study.design.patterns.creational.singleton.classname", "FlexibleSubclassSingleton");
FlexibleExtendableSingleton.activateClass();
FlexibleSubclassSingleton.activateClass();
FlexibleExtendableSingleton singleton = FlexibleExtendableSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is a FlexibleSubclassSingleton.
assertTrue(singleton instanceof FlexibleSubclassSingleton);
}
@AfterEach
protected void tearDown() {
FlexibleExtendableSingleton.clearInstance();
}
}
/////////////////////////////////////////////////////////////////////////////
注意每个单例中的方法“activateClass”。这个方法是空的,似乎什么都不做。实际上,它是第一次触发加载类。调用时,加载类,创建静态单例实例,将条目添加到注册表。如果类没有被加载,注册表将不会被填充,getInstance 会为除基类之外的任何类返回 null,因为调用 getInstance 也会触发加载基类。
或者,您可以使用 ClassLoader 来加载所有单例类,而不是使用“activateClass”方法。您仍然需要显式加载每个单例类。
优点:
getInstance 不必每次都修改。
缺点:
每个子类都需要一个空的 activateClass 方法(或加载类的其他方式),该方法必须在 getInstance 之前调用。由于每个单例类都必须被激活,我们并没有从选项 2 中获得太多改进。
选项 4. 按名称动态加载单例。
在上面的选项 3 中,我们遇到了加载单例类以填充注册表的问题。既然单例的选择已经由系统属性控制,为什么不直接加载要使用的单例类并将 that 设置为实例呢?
使用反射,我们可以按名称加载类,定位静态单例(“thisSingleton”字段),并将其分配给实例。
注意:反射使开发人员能够绕过封装,因此应谨慎使用。在这种情况下,它的使用仅限于 getInstance。
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassdynamicload;
import java.lang.reflect.Field;
//This singleton can be extended (subclassed)
public class DynamicLoadExtendableSingleton {
// The instance - only one of these can exist in the system (currently not accounting for threads).
private static DynamicLoadExtendableSingleton instance;
// This singleton - each class in the hierarchy needs one of these. It will trigger construction (and therefore, registration).
private static DynamicLoadExtendableSingleton thisSingleton = new DynamicLoadExtendableSingleton();
private int sampleValue;
public static DynamicLoadExtendableSingleton getInstance() {
if (instance == null) {
String singletonName = System.getProperty("study.design.patterns.creational.singleton.classname");
ClassLoader loader = DynamicLoadExtendableSingleton.class.getClassLoader();
try {
Class<?> singletonClass = loader.loadClass(singletonName);
Field field = singletonClass.getDeclaredField("thisSingleton");
field.setAccessible(true);
instance = (DynamicLoadExtendableSingleton) field.get(null);
} catch (ClassNotFoundException e) {
// The class was not found.
// TODO: Add error handling code here.
} catch (NoSuchFieldException e) {
// The field does not exist - fix the singleton class to include thisSingleton field.
// TODO: Add error handling code here.
} catch (IllegalAccessException e) {
// Should not occur - we make the field accessible just for this purpose.
// TODO: Add error handling code here.
}
}
return instance;
}
public static void clearInstance() {
// This method wipes out the singleton.
// This is purely for testing purposes so getInstance can reconnect to a new singleton if needed.
instance = null;
}
public int getSampleValue() {
return sampleValue;
}
public void setSampleValue(int sampleValue) {
this.sampleValue = sampleValue;
}
protected DynamicLoadExtendableSingleton() {
// Protected insures construction cannot occur outside of class.
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassdynamicload;
public class DynamicLoadSubclassSingleton extends DynamicLoadExtendableSingleton {
// This singleton - each class in the hierarchy needs one of these. It will trigger construction (and therefore, registration).
private static DynamicLoadSubclassSingleton thisSingleton = new DynamicLoadSubclassSingleton();
private int sampleValue2;
public int getSampleValue2() {
return sampleValue2;
}
public void setSampleValue2(int sampleValue2) {
this.sampleValue2 = sampleValue2;
}
// Must be defined to prevent creation of a public default constructor.
protected DynamicLoadSubclassSingleton() {
super();
}
}
/////////////////////////////////////////////////////////////////////////////
package study.design.patterns.creational.singleton.subclassdynamicload.test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import study.design.patterns.creational.singleton.subclassdynamicload.DynamicLoadExtendableSingleton;
import study.design.patterns.creational.singleton.subclassdynamicload.DynamicLoadSubclassSingleton;
public class DynamicLoadExtendableSingletonTest {
@Test
public void testIllegalCreation() {
// The following lines will not compile because the constructor is not visible.
// DynamicLoadExtendableSingleton instance = new DynamicLoadExtendableSingleton();
// DynamicLoadSubclassSingleton instance2 = new DynamicLoadSubclassSingleton();
}
@Test
public void testCreateDynamicLoadExtendableSingleton() {
System.setProperty("study.design.patterns.creational.singleton.classname", DynamicLoadExtendableSingleton.class.getName());
DynamicLoadExtendableSingleton singleton = DynamicLoadExtendableSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is an ExtendableSingleton, but not a FixedSubclassSingleton.
assertTrue(singleton instanceof DynamicLoadExtendableSingleton);
assertFalse(singleton instanceof DynamicLoadSubclassSingleton);
}
@Test
public void testCreateDynamicLoadSubclassSingleton() {
System.setProperty("study.design.patterns.creational.singleton.classname", DynamicLoadSubclassSingleton.class.getName());
DynamicLoadExtendableSingleton singleton = DynamicLoadExtendableSingleton.getInstance();
assertNotNull(singleton);
// Check that the singleton is a DynamicLoadSubclassSingleton.
assertTrue(singleton instanceof DynamicLoadSubclassSingleton);
}
@AfterEach
protected void tearDown() {
DynamicLoadExtendableSingleton.clearInstance();
}
}
/////////////////////////////////////////////////////////////////////////////
优点:
子类不需要激活类的方法。唯一需要的代码是“thisSingleton”字段。
getInstance 方法不需要对每个新的子类进行修改。
缺点:
反射可能会更慢,但由于它只在一个地方使用并且仅在分配单例时使用,因此风险很小。
如果类名不正确,可能会出错。同样,这是最小的风险。
总之,虽然子类化单例可能并不常见,但 GOF 书中尽可能地解决了它。有几种方法可以支持子类化单例,每种方法都有优点和缺点。上面列出的一些方法直接来自本书。使用反射的方法是我的补充。