TDD學習筆記【五】一隔絕相依性的方式與特性

前言ide

在上一篇文章中,提到了如何經過 IoC 的設計,以及 Stub Object 的方式,來獨立測試目標對象。函數

這一篇文章,則要說明有哪些設計對象的方式,可讓測試或需求變動時,更容易轉換。單元測試

並說明這些方式有哪些特性,供讀者朋友們在設計時,能夠選擇適合本身情境的方式來使用。學習

需求說明

當調用目標對象的方法時,指望目標對象的內容能夠沒必要關注相依於哪些實體對象,而只須要依賴於某個接口,經過這樣的方式來達到設計的彈性與可獨立測試性。測試

那麼,有哪一些方式能夠達到這樣的目的呢?this

構造函數(constructor)

描述:spa

上一篇文章範例所使用的方式,將對象的相依接口,拉到公開的構造函數,供外部對象使用時,可自行組合目標對象的依賴對象實體。.net

public class Validation
{
    private IAccountDao _accountDao;
    private IHash _hash;

    public Validation(IAccountDao dao, IHash hash)
    {
        this._accountDao = dao;
        this._hash = hash;
    }

    public bool CheckAuthentication(string id, string password)
    {
        var passwordByDao = this._accountDao.GetPassword(id);
        var hashResult = this._hash.GetHashResult(password);

        return passwordByDao == hashResult;
    }
}

好處:設計

有許多 DI framework 支持 Autowiring。調試

Autowiring is an automatic detection of dependency injection points.

這裏的 dependency injection points 在這例子,指的就是構造函數。以 Unity 爲例,在 UnityContainer 取得目標對象時,會自動尋找目標對象參數最多的構造函數。並針對每個參數的類型,繼續在 UnityContainer 中尋找對應的實體對象,直到目標對象組合完畢,回傳一個完整的目標對象。

由構造函數傳入依賴接口的實體對象,是一個很通用的方式。所以在結合許多常見的 DI framework,不須要再額外處理。

顧慮點:

當對象愈來愈複雜時,構造函數也會趨於複雜。假若沒有 DI framework 的輔助,則使用對象上,面對許多 overload 的構造函數,或是一個構造函數的參數有好幾個,會形成使用目標對象上的困難與疑惑。若沒有好好進行 refactoring,也可能所以而埋藏許多 bad smell。

另外,假若是許多構造函數,也可能形成要調用 A 方法時,應選用 A 對應的構造函數,但在使用對象上,可能會用錯構造函數而不自知,若方法中沒有正確的防呆,則可能出現錯誤。(請搭配單元測試的測試案例來輔助)

最後,與本來直接依賴的程序代碼相比較,目標對象的相依對象所以暴露出來,交由外部決定,而喪失了一點封裝的意味。而使用端也不必定知道,要取用此對象時,應該要注入哪些相依對象。(請使用 Repository Pattern 或 DI framework 來輔助)

公開屬性(public setter property) 

描述:

其實公開屬性與公開構造函數很是相似,經過 public 的 property(property 類型仍爲 interface),讓外部在使用目標對象時,可先 setting 目標對象的相依對象,接着才調用其方法。

而公開屬性一般只會將 setter 公開給外部設定,getter 則設定爲 private。緣由很簡單,外部只需設定,而不需取用。就像公開構造函數,在使用對象以前先傳入初始化對象必備的信息,但目標對象可能將這些信息,存放在 private 的 filed 或 property 中,而不需再提供給外部使用。

程序代碼以下:

 
public class Validation
{
    public IAccountDao AccountDao { private get; set; }

    public IHash Hash { private get; set; }

    public bool CheckAuthentication(string id, string password)
    {
        if (this.AccountDao == null)
        {
            throw new ArgumentNullException();
        }

        if (this.Hash == null)
        {
            throw new ArgumentNullException();
        }

        var passwordByDao = this.AccountDao.GetPassword(id);
        var hashResult = this.Hash.GetHashResult(password);

        return passwordByDao == hashResult;
    }
}

好處:

一樣的,public property 也是常見的 dependency injection points,因此也有許多 DI framework 支持。另外則是不須要對構造函數進行改變,或增長新的構造函數。對過去已經存在的 legacy code 的影響,會比構造函數的方式小一點點(但幾乎沒有太大差別)。

顧慮點:

最多見的狀況,就是使用目標對象時,依賴接口應有其對應實例,但卻由於使用端沒有設定 public property,致使使用方法時出現 NullReferenceException,這種狀況也怪不了使用端,由於使用端極有可能本就不瞭解這個方法中,有哪些依賴對象。

解決方式與構造函數的建議雷同,首先固然要有測試程序來講明(測試程序就是對象使用說明書),另外取得目標對象,仍可經過 Repository Pattern,讓使用端無須瞭解目標對象的相依關係。

而且在方法中使用依賴接口前,應檢查其是否爲 null,若爲 null,則表明參數設定錯誤,進行 error handling,避免已經發生錯誤仍執行許多不該執行的程序代碼。或是在 property 的 getter 時,檢查是否爲 null 或當爲 null 時,給予一默認值,以免方法沒法正常執行。(視實際需求而定)

另外,公開屬性的方式,也如同公開構造函數通常,破壞了一點點對象封裝的用意。但這二者,都是 IoC 設計會帶來的影響。

調用方法時傳入參數(function parameter)

描述:

既然前面兩種方式,均可能形成使用方法時,可能沒有設定好依賴接口的實例,致使發生錯誤。或是使用目標對象時,不知道該調用哪個構造函數或初始化哪些屬性。那很簡單的方式,就是把方法依賴接口的部分,拉到方法的參數上。方法中,須要使用到哪些接口,強迫由調用端必須給定參數。目標對象的方法內容則僅依賴於參數上的接口。

程序代碼以下:

public bool CheckAuthentication(IAccountDao accountDao, IHash hash, string id, string password)
{
    var passwordByDao = accountDao.GetPassword(id);
    var hashResult = hash.GetHashResult(password);

    return passwordByDao == hashResult;
}

好處:

沒必要再擔憂要先初始化哪些 property,或調用哪個構造函數。當要調用某一個方法,其相依的對象,就是得經過參數來給定。基本上也不太須要擔憂使用上形成困擾或迷惑。

顧慮點:

最大的問題,在於方法簽名上的不穩定性。當需求異動,該方法須要額外相依於其餘對象時,方法簽名可能會被迫改變。而方法簽章是面向對象設計上,最須要穩定的條件之一。以面向對象、接口導向設計來講,當多態對象方法簽名不一致時,向來是個大問題。

另外,方法的參數過多,在使用上也會形成困擾。並且會影響到 legacy code 的調用端,須要全面跟着異動,才能編譯成功。

並且經過參數的方式,DI framework 支持度較低。

但這不表明,就不能在方法參數中,傳入相依對象。在 .net framework 仍是有許多這樣的設計,例如:List<T>.Sort 方法 (IComparer<T>)這樣的設計方式,一般要確保該方法相依至關明確、穩固,避免上述問題。

by the way, 這個方式是能夠與其餘方式共存的,因此在設計對象時,可衡量搭配使用。

可覆寫的保護方法(protected virtual function)

描述:

前面的三種方式,基本上都對外暴露了本來可能不須要對外暴露的細節。假若,如今的需求是眼前的程序要進行測試,但又不但願影響或修改使用端的程序,那麼該怎麼做呢?除了能夠透過公開屬性設定,當爲空時給予默認值的方式,來維持本來對象的內部程序邏輯之外,還有一個至關簡單的方式,甚至有些狀況不須要透過接口設計,就能夠進行測試。先來看看本來直接依賴對象,沒法測試的程序,程序代碼以下:

public class Validation
{
    public bool CheckAuthentication(string id, string password)
    {
        var accountDao = new AccountDao();
        var passwordByDao = accountDao.GetPassword(id);

        var hash = new Hash();
        var hashResult = hash.GetHashResult(password);

        return passwordByDao == hashResult;
    }
}

接下來,咱們只用簡單的面向對象概念:繼承、重寫,就能夠對 Validation 對象的 CheckAuthentication 方法進行測試。不相信嗎?繼續往下看下去。

首先,必定要記得,把 new 對象的動做抽離高層抽象的 context(能夠透過 extract method 的方式抽離)程序代碼以下

public class Validation
{
    public bool CheckAuthentication(string id, string password)
    {
        var accountDao = GetAccountDao();
        var passwordByDao = accountDao.GetPassword(id);

        var hash = GetHash();
        var hashResult = hash.GetHashResult(password);

        return passwordByDao == hashResult;
    }

    private Hash GetHash()
    {
        var hash = new Hash();
        return hash;
    }

    private AccountDao GetAccountDao()
    {
        var accountDao = new AccountDao();
        return accountDao;
    }
}

沒什麼改變,對吧?

接下來,將兩個 new 對象的方法,聲明爲 protected virtual,表明子類別能夠繼承與重寫該方法。程序代碼以下:

protected virtual Hash GetHash()
{
    var hash = new Hash();
    return hash;
}

protected virtual AccountDao GetAccountDao()
{
    var accountDao = new AccountDao();
    return accountDao;
}

另外,將要使用到 Hash 與 AccountDao 的方法,也要聲明爲 virtual程序代碼以下:

public class AccountDao
{
    public virtual string GetPassword(string id)
    {
        throw new NotImplementedException();
    }
}

public class Hash
{
    public virtual string GetHashResult(string password)
    {
        throw new NotImplementedException();
    }
}

到這裏,都不影響外部使用目標對象的行爲,咱們只是在重構對象的內部方法罷了。事實上,咱們可測試性的動做也準備完畢了。(固然,建議仍是要依賴於接口,實現接口要顧慮的點,比繼承類要輕鬆的多)

接下來把目光切到測試程序,該如何對 CheckAuthentication 方法進行測試。

首先,將上一篇文章的 StubHash 改成繼承自 Hash,StubAccountDao 改成繼承自 AccountDao,並將本來 public 的方法,加上 override 關鍵詞,重寫其父類方法內容。程序代碼以下:

public class StubAccountDao : AccountDao
{
    public override string GetPassword(string id)
    {
        return "Hello World";
    }
}

public class StubHash : Hash
{
    public override string GetHashResult(string password)
    {
        return "Hello World";
    }
}

不難,對吧。接下來,創建一個 MyValidation 的 class,繼承自 Validation。並重寫 GetAccountDao() 與 GetHash(),使其回傳 Stub Object。程序代碼以下:

public class MyValidation : Validation
{
    protected override AccountDao GetAccountDao()
    {
        return new StubAccountDao();
    }

    protected override Hash GetHash()
    {
        return new StubHash();
    }
}

也不難,對吧。接下來,來設計單元測試,程序代碼以下:

[TestMethod()]
public void CheckAuthenticationTest()
{
    Validation target = new MyValidation();

    string id = "id隨便";
    string password = "密碼也隨便";

    bool expected = true;

    bool actual;
    actual = target.CheckAuthentication(id, password);

    Assert.AreEqual(expected, actual);
}

本來初始化的測試目標爲 Validation 對象,如今則爲 MyValidation 對象。裏面惟一不一樣的部分,只有重寫的方法內容,其他 MyValidation 就等同於 Validation。Is-A的關係)調試測試一下,就能夠確認,程序代碼就跟以前使用 IoC 的方式執行沒有太大的差別。

好處:

這個方式最大的好處,是徹底不影響外部使用對象的方式。僅透過 protected 與 virtual 來對繼承鏈開放擴充的功能,而且透過這樣的方式,就使得本來直接相依而致使沒法測試的問題,得到解套。

顧慮點:

這是爲了測試,且面對 legacy code 所使用的方式,而不是良好的面向對象設計的方式。IoC 的用意在於面向藉口與擴充點的彈性,因此當可測試以後,假若重構影響範圍不大,建議讀者朋友仍是要將對象改依賴於接口,經過IoC 的方式來設計對象。

by the way, 一樣爲了解決直接相依對象,甚至相依於 static 方法、.net framework 自己的對象(如 DateTime.Now)而致使沒法測試的問題,還有另一個方式,稱爲 fake object。這在後面的文章,會再進行較爲詳盡的介紹。

 結論

以上幾種用來測試的方式,但願對各位讀者在不一樣情境下的設計,能夠有所幫助。

而許多延伸的議題,在這系列文章並不會多談,但在實務應用面上,倒是至關重要的配套措施。例如一再提到的 DI framework, Repository Pattern,以及經過測試程序來講明對象的使用方式,請讀者在現實設計系統時,務必瞭解這些東西如何讓系統設計更加完整。

下一篇文章,將介紹怎麼樣能夠避免每次手工敲打這麼囉唆的 stub 對象,怎麼針對 static 或 .net framework 自己的對象進行隔離,怎麼針對對象與相依接口互動的狀況進行測試。

 

備註:這個系列是我畢業後時隔一年從新開始進入開發行業後對大拿們的博文摘要整理進行學習對自個人各個欠缺的方面進行充電記錄博客的過程,非原創,特此感謝91 等前輩

相關文章
相關標籤/搜索