整理Effective Java書中Item 3: Enforce the singleton property with a private constructor or an enum type心得筆記

主旨

本篇在介紹單例模式(singleton pattern)以及相關缺失防範。

singleton 常見方法

  1. 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) {
            }
        }
    });
  1. 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() { ... }

}
  1. 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也讓隊友一直踩坑。

參考延伸閱讀: