国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計 > 正文

TDD學(xué)習(xí)筆記【五】一隔絕相依性的方式與特性

2019-11-14 16:27:35
字體:
供稿:網(wǎng)友

前言

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

這一篇文章,則要說明有哪些設(shè)計對象的方式,可以讓測試或需求變更時,更容易轉(zhuǎn)換。

并說明這些方式有哪些特性,供讀者朋友們在設(shè)計時,可以選擇適合自己情境的方式來使用。

需求說明

當(dāng)調(diào)用目標對象的方法時,期望目標對象的內(nèi)容可以不必關(guān)注相依于哪些實體對象,而只需要依賴于某個接口,通過這樣的方式來達到設(shè)計的彈性與可獨立測試性。

那么,有哪一些方式可以達到這樣的目的呢?

構(gòu)造函數(shù)(constructor)

描述:

上一篇文章范例所使用的方式,將對象的相依接口,拉到公開的構(gòu)造函數(shù),供外部對象使用時,可自行組合目標對象的依賴對象實體。

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 在這例子,指的就是構(gòu)造函數(shù)。以 Unity 為例,在 UnityContainer 取得目標對象時,會自動尋找目標對象參數(shù)最多的構(gòu)造函數(shù)。并針對每一個參數(shù)的類型,繼續(xù)在 UnityContainer 中尋找對應(yīng)的實體對象,直到目標對象組合完畢,回傳一個完整的目標對象。

由構(gòu)造函數(shù)傳入依賴接口的實體對象,是一個很通用的方式。因此在結(jié)合許多常見的 DI framework,不需要再額外處理。

顧慮點:

當(dāng)對象越來越復(fù)雜時,構(gòu)造函數(shù)也會趨于復(fù)雜。倘若沒有 DI framework 的輔助,則使用對象上,面對許多 overload 的構(gòu)造函數(shù),或是一個構(gòu)造函數(shù)的參數(shù)有好幾個,會造成使用目標對象上的困難與疑惑。若沒有好好進行 refactoring,也可能因此而埋藏許多 bad smell。

另外,倘若是許多構(gòu)造函數(shù),也可能造成要調(diào)用 A 方法時,應(yīng)選用 A 對應(yīng)的構(gòu)造函數(shù),但在使用對象上,可能會用錯構(gòu)造函數(shù)而不自知,若方法中沒有正確的防呆,則可能出現(xiàn)錯誤。(請搭配單元測試的測試案例來輔助)

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

公開屬性(public setter property) 

描述:

其實公開屬性與公開構(gòu)造函數(shù)非常類似,通過 public 的 property(property 類型仍為 interface),讓外部在使用目標對象時,可先 setting 目標對象的相依對象,接著才調(diào)用其方法。

而公開屬性通常只會將 setter 公開給外部設(shè)定,getter 則設(shè)定為 private。原因很簡單,外部只需設(shè)定,而不需取用。就像公開構(gòu)造函數(shù),在使用對象之前先傳入初始化對象必備的信息,但目標對象可能將這些信息,存放在 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 支持。另外則是不需要對構(gòu)造函數(shù)進行改變,或增加新的構(gòu)造函數(shù)。對過去已經(jīng)存在的 legacy code 的影響,會比構(gòu)造函數(shù)的方式小一點點(但幾乎沒有太大差異)。

顧慮點:

最常見的情況,就是使用目標對象時,依賴接口應(yīng)有其對應(yīng)實例,但卻因為使用端沒有設(shè)定 public property,導(dǎo)致使用方法時出現(xiàn) NullReferenceException,這種情況也怪不了使用端,因為使用端極有可能本就不了解這個方法中,有哪些依賴對象。

解決方式與構(gòu)造函數(shù)的建議雷同,首先當(dāng)然要有測試程序來說明(測試程序就是對象使用說明書),另外取得目標對象,仍可通過 Repository Pattern,讓使用端無須了解目標對象的相依關(guān)系。

并且在方法中使用依賴接口前,應(yīng)檢查其是否為 null,若為 null,則代表參數(shù)設(shè)定錯誤,進行 error handling,避免已經(jīng)發(fā)生錯誤仍執(zhí)行許多不應(yīng)執(zhí)行的程序代碼。或是在 property 的 getter 時,檢查是否為 null 或當(dāng)為 null 時,給予一默認值,以避免方法無法正常執(zhí)行。(視實際需求而定)

另外,公開屬性的方式,也如同公開構(gòu)造函數(shù)一般,破壞了一點點對象封裝的用意。但這兩者,都是 IoC 設(shè)計會帶來的影響。

調(diào)用方法時傳入?yún)?shù)(function parameter)

描述:

既然前面兩種方式,都可能造成使用方法時,可能沒有設(shè)定好依賴接口的實例,導(dǎo)致發(fā)生錯誤。或是使用目標對象時,不知道該調(diào)用哪一個構(gòu)造函數(shù)或初始化哪些屬性。那很簡單的方式,就是把方法依賴接口的部分,拉到方法的參數(shù)上。方法中,需要使用到哪些接口,強迫由調(diào)用端必須給定參數(shù)。目標對象的方法內(nèi)容則僅依賴于參數(shù)上的接口。

程序代碼如下:

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

好處:

不必再擔(dān)心要先初始化哪些 property,或調(diào)用哪一個構(gòu)造函數(shù)。當(dāng)要調(diào)用某一個方法,其相依的對象,就是得通過參數(shù)來給定。基本上也不太需要擔(dān)心使用上造成困擾或迷惑。

顧慮點:

最大的問題,在于方法簽名上的不穩(wěn)定性。當(dāng)需求異動,該方法需要額外相依于其他對象時,方法簽名可能會被迫改變。而方法簽章是面向?qū)ο笤O(shè)計上,最需要穩(wěn)定的條件之一。以面向?qū)ο蟆⒔涌趯?dǎo)向設(shè)計來說,當(dāng)多態(tài)對象方法簽名不一致時,向來是個大問題。

另外,方法的參數(shù)過多,在使用上也會造成困擾。而且會影響到 legacy code 的調(diào)用端,需要全面跟著異動,才能編譯成功。

而且通過參數(shù)的方式,DI framework 支持度較低。

但這不代表,就不能在方法參數(shù)中,傳入相依對象。在 .net framework 還是有許多這樣的設(shè)計,例如:List<T>.Sort 方法 (IComparer<T>)這樣的設(shè)計方式,通常要確保該方法相依相當(dāng)明確、穩(wěn)固,避免上述問題。

by the way, 這個方式是可以與其他方式共存的,所以在設(shè)計對象時,可衡量搭配使用。

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

描述:

前面的三種方式,基本上都對外暴露了原本可能不需要對外暴露的細節(jié)。倘若,現(xiàn)在的需求是眼前的程序要進行測試,但又不希望影響或修改使用端的程序,那么該怎么作呢?除了可以透過公開屬性設(shè)定,當(dāng)為空時給予默認值的方式,來維持原本對象的內(nèi)部程序邏輯以外,還有一個相當(dāng)簡單的方式,甚至有些情況不需要透過接口設(shè)計,就可以進行測試。先來看看原本直接依賴對象,無法測試的程序,程序代碼如下:

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;    }}

接下來,我們只用簡單的面向?qū)ο蟾拍睿豪^承、重寫,就可以對 Validation 對象的 CheckAuthentication 方法進行測試。不相信嗎?繼續(xù)往下看下去。

首先,一定要記得,把 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();    }}

到這里,都不影響外部使用目標對象的行為,我們只是在重構(gòu)對象的內(nèi)部方法罷了。事實上,我們可測試性的動作也準備完畢了。(當(dāng)然,建議還是要依賴于接口,實現(xiàn)接口要顧慮的點,比繼承類要輕松的多)

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

首先,將上一篇文章的 StubHash 改為繼承自 Hash,StubAccountDao 改為繼承自 AccountDao,并將原本 public 的方法,加上 override 關(guān)鍵詞,重寫其父類方法內(nèi)容。程序代碼如下:

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

也不難,對吧。接下來,來設(shè)計單元測試,程序代碼如下:

[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 對象,現(xiàn)在則為 MyValidation 對象。里面唯一不同的部分,只有重寫的方法內(nèi)容,其余 MyValidation 就等同于 Validation。Is-A的關(guān)系)調(diào)試測試一下,就可以確認,程序代碼就跟之前使用 IoC 的方式執(zhí)行沒有太大的差異。

好處:

這個方式最大的好處,是完全不影響外部使用對象的方式。僅透過 protected 與 virtual 來對繼承鏈開放擴充的功能,并且透過這樣的方式,就使得原本直接相依而導(dǎo)致無法測試的問題,獲得解套。

顧慮點:

這是為了測試,且面對 legacy code 所使用的方式,而不是良好的面向?qū)ο笤O(shè)計的方式。IoC 的用意在于面向借口與擴充點的彈性,所以當(dāng)可測試之后,倘若重構(gòu)影響范圍不大,建議讀者朋友還是要將對象改依賴于接口,通過IoC 的方式來設(shè)計對象。

by the way, 同樣為了解決直接相依對象,甚至相依于 static 方法、.net framework 本身的對象(如 DateTime.Now)而導(dǎo)致無法測試的問題,還有另外一個方式,稱為 fake object。這在后面的文章,會再進行較為詳盡的介紹。

 結(jié)論

以上幾種用來測試的方式,希望對各位讀者在不同情境下的設(shè)計,可以有所幫助。

而許多延伸的議題,在這系列文章并不會多談,但在實務(wù)應(yīng)用面上,卻是相當(dāng)重要的配套措施。例如一再提到的 DI framework, Repository Pattern,以及通過測試程序來說明對象的使用方式,請讀者在現(xiàn)實設(shè)計系統(tǒng)時,務(wù)必了解這些東西如何讓系統(tǒng)設(shè)計更加完整。

下一篇文章,將介紹怎么樣可以避免每次手工敲打這么啰唆的 stub 對象,怎么針對 static 或 .net framework 本身的對象進行隔離,怎么針對對象與相依接口互動的情況進行測試。

 

備注:這個系列是我畢業(yè)后時隔一年重新開始進入開發(fā)行業(yè)后對大拿們的博文摘要整理進行學(xué)習(xí)對自我的各個欠缺的方面進行充電記錄博客的過程,非原創(chuàng),特此感謝91 等前輩


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 铜鼓县| 贺兰县| 天台县| 西林县| 东丰县| 河间市| 靖远县| 沧源| 彭泽县| 日土县| 扎兰屯市| 山阳县| 土默特右旗| 如东县| 呼玛县| 汤原县| 葫芦岛市| 甘南县| 滕州市| 峡江县| 宁夏| 昆山市| 镇原县| 安溪县| 江川县| 原平市| 新邵县| 桑日县| 和静县| 阜新| 那坡县| 万安县| 深泽县| 东海县| 盈江县| 巨野县| 崇信县| 屯留县| 长岛县| 七台河市| 铜陵市|