Global State 是什麼?Singleton 為何讓人抓狂?

Yushan
6 min readMar 10, 2018

--

什麼是 Global State?

一種偷偷影響程式碼執行的方式。

當執行相同的程式時,一般情況下會預期跑出同樣的結果;但如果使用了 Global State,每次執行時就無法預期結果。

Global State 不好嗎?

大部份情況下,盡量不要使用。因為 Global State 令人難以理解程式為什麼這麼運作,也很難 debug 和測試,因為你不知道它到底在背後做了什麼。

可以來個例子嗎?

拿 Design Pattern 中的 Singleton 為例,Singleton 本身就是個 Global State,用到此 singleton instance 的物件彼此會造成影響。

class AppSettings {
private static AppSettings instance = new AppSettings();
private Object state1;
private Object state2;
private Object state3;
private AppSettings() {...}
public static AppSettings getInstance() {
return instance;
}
}
class App {
int method() {
return AppSettings.getInstance().changeState1();
}
}
void testApp() {??}

這樣寫有什麼問題?

  1. 高耦合:假設 A 和 B 兩個不相關的物叫都會使用到 AppSettings,結果 A 不小心把 AppSettings 寫壞了,B 也跟著中槍。這種改 A 炸 B 的 bug 是很難被找出來的,只能回想我之前是不是做了什麼事來推測 B 為什麼會壞掉。
  2. 無法測試:因為 AppSettings.getInstance() 是 Singleton,它可能在前一次測試時被改變了 state1~3,但 testApp() 無從知道,所以每次拿到的 參數都不同,導致測試的結果也都不同。
  3. 無邊界:Singleton 就是個全域變數,它架起了類與類之間的橋樑,模糊了關界。

如何修正?

class App {
AppSettings settings;
App(AppSettings settings) {
this.settings = settings;
}
int method() {
return settings.changeState1();
}
}
void testApp() {
new App(new AppSettings(...)).method();
}

這輩子真的無法再使用 Singleton 了嗎?QQ

Singleton 還是有好用的地方的,但僅限於 immutable 不會影響到其它的物件的情況,像是:

  • System.currenTime()
  • new Date()
  • Math.random()
  • 記錄 log

除了 Singleton,還有別的例子嗎?

有的,設計 API 時也有可能因為 Global State 造成使用上的各種困擾。我們稱這種 API 為 Deceptive API。

假設我們有一個信用卡扣款的程式,當工程師要來寫 unit test 時,他呼叫了以下的程式

testCharge() {
CreditCard cc;
cc = new CreditCard("123482328523423"); //創建一張信用卡
cc.carge(100); //扣 100 元
}

看起來非常簡單,但 compiler 後才發現不能跑,因為它缺少了各種資料!

testCharge() {
Database.connect(...);
OfflineQueue.start();
CreditCardProcessor.init(...);
CreditCard cc;
cc = new CreditCard("123482328523423"); //創建一張信用卡
cc.carge(100); //扣 100 元
}

首先需要 DB 連線,然後建立離線的 queue,然後開始跑信用卡的 processor……我怎麼知道我需要這些東西才能扣款!?

這就是 API 設計不良的範例,它假裝不需要任何東西,但實際上是需要的!只是透過 Global State 來傳送參數。

那要怎麼改工程師才不會抓狂?

testCharge() {
db = new Database(...);
queue = new OfflineQueue(db);
ccProc = new CreditCardProcessor(queue);
CreditCard cc;
cc = new CreditCard("123482328523423", ccProc); //創建一張信用卡
cc.carge(100); //扣 100 元
}

不再使用 Global State,而改用注入的方式(Dependency Injection),把需要的東西從外面帶入,降低耦合。

這樣做有什麼好處呢?

  1. 清楚地知道呼叫 function 前我需要什麼、清楚地知道我要依序做什麼動作才不會出錯,而不是當 compiler 時才出現各種錯誤。
  2. 每一行都可以獨立 unit test,例如我可以只要 mock 一個 CreditCardProcessor 就能測試信用卡扣款是否正常。或是我需要真的 CreditCardProcessor、真的 queue、但是要用假的 database,在 DI 的情況下任君挑選如何做測試。

總結

  • Global State 是所有測試問題的根源。
  • Global State 無法完全由 test 控制,有可能另一個人寫了新的 test 影響到了 Global State,造成原本的測試失敗。
  • Singleton 使用需小心,不能因為我這裡需要個只能創建一次的物件,就很快樂地使用 Singleton,還是有其它的方式可以達到相同的目的。

避免 Global State 的產生除了讓程式有辦法寫測試外,還可以大幅降低需要通靈 debug 的機會……

參考資料

https://www.ptt.cc/bbs/GameDesign/M.1505060613.A.66F.html
https://www.youtube.com/watch?v=-FRm3VPhseI
https://www.jianshu.com/p/b3f5f3392c0f

--

--