該系列第1篇: 講述了如何創造"縫". "縫"(seam)是須要知道的概念.html
本文是第2篇, 介紹的是如何避免在構建對象時寫出不易測試的代碼. 本文的概念性內容大部分都來自Misko Hevery的這篇博客文章.ide
仍是用上文裏汽車的例子.函數
一般狀況下, 咱們是先去建造汽車, 組裝好汽車後, 咱們再去駕駛它.測試
軟件開發也相似, 咱們應該把對象構造完畢以後, 再去用它. 可是有時候, 開發者會在構造過程當中添加一些程序邏輯. 這就至關於車還沒造完, 咱們就駕駛它去兜風了. 這樣作是不太好的.ui
構造函數是類用來建立其實例對象的方法, 這裏的代碼是用來準備該對象的. 但有時開發者會在構造函數裏作一些其它的工做, 例如構建依賴項, 執行初始化邏輯等等.spa
在構造函數(或者更大一點, 指構建的過程)裏, 作這些額外的工做會讓測試變得異常困難. 這是由於像初始化依賴項, 調用服務, 設置狀態的邏輯等這些工做會把用於測試的"縫"弄丟. 致使沒法進行mock.3d
總之在構造的過程當中作太多的工做會妨礙測試.code
總之就是要避免對象的構建和對象的行爲混合到一塊兒, 由於它們在一塊兒就會很難進行測試.htm
最後還有一點, 首先你須要知道, 根據angular的創始人Misko Hevery所說:對象
對象的構造分兩類, 一種是可注入的, 一種是可new的.
可注入的對象能夠由其它的一堆可注入對象組成. 它們能夠爲 可new的 對象工做. 可注入的對象一般是實現了接口的service, 像什麼IUnitOfWork, IRepository, IxxxService等等.
可new的對象就是對象圖裏的終點, 例如實體或者值對象(Value Object)等.
爲了易於測試, 針對這兩類構造, 有下列規則:
可注入的對象能夠在構造函數請求(注入)其它的能夠注入對象, 可是不能在構造函數請求可new的對象.
反過來, 可new的對象能夠在構造函數請求其它的可new對象, 可是不能在構造函數請求可注入的對象.
這是不對的, 構建的過程當中直接new的話, 就會形成緊耦合, 也沒法在測試中使用Test Double來代替它們了. 若是測試中不代替它們的話, 有些服務的開銷可能會很大.
正確的寫法是使用依賴注入:
該例中, UserController只須要UserService和LoggingService兩個依賴項. 可是UserService又依賴於UserRepository.
可是這樣寫就不對了, 這會形成UserController和UserRepository間的緊耦合, 並且配置UserService也並非UserController的責任.
正確的寫法是:
而UserService也最好是注入依賴.
而若是UserService並非在構造函數注入UserRepository的話:
那麼Controller裏就應該這樣寫:
不過最好仍是使用構造函數注入的寫法.
仔細的說, 該例有不止一處錯誤.
首先它有條件判斷邏輯代碼; 此外它還使用了ApplicationState.IsRunning這個靜態變量(就是全局狀態); 並且在構造函數裏還作了UserService的配置工做, 這不是UserController的責任.
儘可能要避免全局變量, 它沒法進行隔離, 測試會遇到麻煩, 例如並行測試時其中一個測試改變了靜態變量的值就可能致使另外一個測試失敗.
可是粗略的說, 該例能夠說就是一個錯誤, 如何配置UserService並非UserController的責任, 因此, 正確的作法是把UserService配置相關的代碼移出去, 讓它本身去管理吧:
該例子中, LoggingService的Log方法須要一個Area類型的對象, 它是一個值對象.
因此它的錯誤就是, 不該該把可new的對象注入到可注入的對象裏. 這麼作的話, 測試就很差作隔離了.
正確的作法應該是, 做爲方法的參數傳遞進來:
若是出現類相似initalize()或相似意思的方法, 頗有可能說明該對象的責任太多了.
修改它很簡單, 讓各自的類負責本身的內容便可. 去掉initialize()方法便可.
例子就舉這些, 並不全, 詳細請看Angular做者的博文.
上面例子裏的UserController就是咱們須要使用的對象, 在運行時, 代碼多是這樣的:
構建這個對象仍是有點麻煩的, 它的類關係圖以下:
因此測試的設置過程也會比較麻煩:
固然也能夠不直接new, 而是使用mock. 總之都很麻煩.
因此咱們可使用Factory等模式, 把構建UserController的工做放到工廠裏:
能夠這樣調用:
若是項目使用了IoC容器的話, 還可使用相似下面的用法:
先介紹到這裏.