第1篇: 講述了如何創造"縫". "縫"(seam)是需要知道的概念.
第2篇, 避免在構建物件時寫出不易測試的程式碼.
第3篇, 依賴項和迪米特法則.
本文是第4篇, 將介紹全域性狀態引起的問題.
全域性狀態
全域性狀態, 也可以叫做應用程式狀態, 它是一組變數, 這些變數維護著應用程式的高階狀態.
在程式裡, 全域性狀態可能都存放在一個全域性狀態物件裡, 例如ASP.NET裡面的HttpContext; 或者它們可能是全域性的變數, 這些全域性變數在程式的任何地方都可以訪問.
不管是如何實現的全域性狀態, 每個全域性狀態變數在記憶體裡只有一個例項. 所以如果一個類裡更新了全域性變數的值, 那麼另一個類訪問該變數的時候它的值就是剛才被更新的值.
有些情況下, 使用全域性狀態確實有用; 但是如果使用不當, 則會對測試造成很大的影響.
全域性狀態對測試引起的問題
- 使用靜態方法或全域性變數訪問全域性狀態的時候, 就引起了對全域性狀態的直接耦合. 這很不好.
- 這種耦合就導致很難對測試進行設定. 針對每個測試, 我們必須建立和設定好儲存全域性狀態的物件. 或者把全域性變數設定為所需的值.
- 因為每個全域性狀態變數在記憶體裡只有一個例項, 那麼我們就無法進行並行單元測試了. 如果我們為A測試設定了全域性變數的值, 然後在測試A結束前開始測試B, 這時測試B修改了全域性變數的值, 這時測試A就可能會失敗, 因為它所期待的全域性變數不是這個值.
- 上面的這種現象就叫做鬼魅般的超距作用(Spooky Action at a Distance). 而實際專案中確實經常發生這樣的情況, 並行跑單元測試的時候偶爾會失敗, 而單獨去跑失敗的測試時卻一直成功. 這種耦合到全域性狀態的測試就不能再稱為隔離測試了.
危險訊號
- 全域性變數
- 呼叫靜態欄位或呼叫擁有靜態欄位的類的靜態方法. 但也僅限於該類的靜態方法使用了該類的靜態欄位.
- 單例模式 (Singleton Pattern)
- 單元測試會隨機的失敗, 但是又沒發現明確的原因.
解決辦法
- 儘量使用本地(區域性, 越窄越好)狀態變數
- 如果第三方庫使用了靜態方法, 那麼應該使用一個包裝類來對該方法進行包裝. 這個包裝類還是要實現一個介面. 用它的時候注入該介面即可. 這樣測試的時候就可以為包裝類建立測試替身了, 並把全域性狀態解耦.
- 使用可依賴注入(IoC/DI)的單例體, 這種單例體是由IoC容器建立的.
例子
就舉一個例子吧.
有這樣一個獲取當前登入使用者許可權的類, 它使用的是單例模式:
這個是典型的單例模式, 它會保證在程式中只返回一個例項, 這裡就不多介紹了.
下面這個Service會呼叫上面這個Auth類:
Auth是單例模式的, 而且還呼叫了靜態方法.
現在的狀態是, OfficeService和Auth所包含的全域性狀態緊密的耦合到了一起.
如何解決問題
首先應該把單例模式去掉, Auth類只保留兩個屬性和一個方法:
然後在service裡面應該注入IAuth介面並使用:
那麼接下來就需要保證這個IAuth無論在程式中注入了多少次, 都是同一個例項.
這時就需要使用依賴注入(DI) 庫了. 現在的DI庫通常允許指定IoC容器中每對繫結服務的作用範圍(Scope), 或叫做生命週期管理.
例如ASP.NET Core內建的IoC容器就內建了這種功能. 在ASP.NET Core 專案的Startup類裡, 這樣寫就可以保證每次請求IAuth的時候只會得到同一個物件例項:
現在這個"單例"的工作是由IoC容器來負責了. 在其它地方正常的注入IAuth使用即可.
先寫到這, 本文的概念性內容和更多的例子請參考Angular創始的人這篇文章: http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/