Effective Java Item3 使用私有建構函式或列舉實現單例
整理Effective Java書中Item 3: Enforce the singleton property with a private constructor or an enum type心得筆記
主旨
本篇在介紹單例模式(singleton pattern)以及相關缺失防範。
singleton 常見方法
- private constructor和static final INSTANCE
public class Cache {
public static final Cache INSTANCE = new Cache();
private Cache() { ... }
public List<String> getBanks() { ... }
}
雖然可以使用private constructor透過singleton pattern創造唯一的instance,但是不是絕對保證的,還是可以透過AccessibleObject.setAccessible
經由反射(Reflection)原理調用私有建構函式,如果要防範這種hack行為,可以在創建第2個實例丟出exception。
Constructor<?>[] constructors = Cache.class.getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);
Arrays.stream(constructors).forEach(name -> {
if (name.toString().contains("Cache")) {
try {
Cache instance = (Cache) name.newInstance();
List<String> banks = nstance.getBanks();
} catch (Exception e) {
}
}
});
- static factory method
public class Cache {
private static final Cache INSTANCE = new Cache();
private Cache() { ... }
public static Cache getInstance() { return INSTANCE; }
public List<String> getBanks() { ... }
}
使用static factory method實現singleton有幾個優點
- 比較彈性控制是否這個class為singleton。
- 可以設計為generic
- 可以設計支持supplier
我們在設計一個singleton要把相關可能的缺失都補起來的防禦式開發,防止透過Reflection、Serialization以及Clone創造第2個instance。Reflection的部分就像之前提到的AccessibleObject.setAccessible
的解法。再者如果今天這個class implements Serializable,他就不再是Singleton了,因為在de-serialize後會創造一個新的instance,同時調用clone同時也會有新的instance問題。為了防範上述問題可以參考下面範例。首先constructor檢查是否已經存在並拋出錯誤,override clone()拋出不支持錯誤,在readResolve直接返回目前instance。
public class Cache implements Cloneable, Serializable {
private static final long serialVersionUID = 5016600873291582535L;
private static final Cache INSTANCE = new Cache();
private Cache() {
if (Cache.INSTANCE != null) {
throw new InstantiationError("The INSTANCE aleady ceated.");
}
}
public static Cache getInstance() {
return INSTANCE;
}
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
protected Object readResolve() {
return INSTANCE;
}
public List<String> getBanks() { ... }
}
- enum
public enum Cache {
INSTANCE;
public List<String> getBanks() { ... }
}
使用enum是比較推薦實現singleton的作法,簡單又沒有上面那些問題。
問題探討
文中提到Making a class a singleton can make it difficult to test its clients是什麼意思呢?
這是Singleton本身是Global State所造成的問題,舉個例子:
@Test
public void testSend() {
EthService service = new EthService();
EthTransaction tx = new EthTransaction.Builder()
.setNonce(BigInteger.valueOf(0L))
.setGasPrice(BigInteger.valueOf(5000000000L))
.setGasLimit(BigInteger.valueOf(150000L))
.setTo("xxxxxxxxx")
.setValue(BigInteger.valueOf(1000000000000000000L));
System.out.println(service.broadcast(tx));
}
在上面unit test第1個問題遇到了NullPointerException,這個程式居然沒辦法測試,往裡面追了才知道,使用之前需要先執行Eth.connect();KeyStore.init();
,像這種必須有依賴關係的情況在使用上完全看不出來,Eth和KeyStore的singleton global state造成測試困難,第2個問題在於另一個開發同仁新增了一個新的test影響了這個的測試,他在他的test測試中執行了KeyStore.removeAll();
導致原本的testSend()發生錯誤,這都是使用singleton很容易輕忽的地方,如果可以最好改成注入的方式(Dependency Injection)設計。不要一直爽用singleton也讓隊友一直踩坑。
參考延伸閱讀: