iT邦幫忙

DAY 6
6

30天快速上手TDD系列 第 6

[Day 6]隔絕相依性的方式與特性

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

這一篇文章,則要說明有哪些設計物件的方式,可以讓測試或需求異動時,更容易抽換。

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

上一篇文章:[Day 5]如何隔離相依性 - 基本的可測試性
本系列文章專區
當呼叫目標物件的方法時,期望目標物件可以不必關注相依於哪些實體物件,而只需要相依於某個介面,透過這樣的方式來達到設計的彈性與可獨立測試性。

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

@建構式(constructor)

  1. 描述:
    上一篇文章範例所使用的方式,將物件的相依介面,拉到公開的建構式,供外部物件使用時,可自行組合目標物件的相依物件實體。

程式碼如下:

    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;
        }
    }
  1. 好處:
    有許多DI framework支援Autowiring。

Autowiring is an automatic detection of dependency injection points

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

由建構式傳入相依介面的實體物件,是一個很通用的方式。因此在結合許多常見的DI framework,不需要在額外處理。

  1. 顧慮點:
    當物件越來越複雜時,建構式也會趨於複雜。倘若沒有DI framework的輔助,則使用物件上,面對許多overload的建構式,或是一個建構式參數有好幾個,會造成使用目標物件上的困難與疑惑。若沒有好好進行refactoring,也可能因此而埋藏許多bad smell。

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

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

@公開屬性(public setter property)

  1. 描述:
    其實公開屬性與公開建構式非常類似,透過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;
        }
    }
  1. 好處:
    同樣的,public property也是常見的dependency injection points,所以也有許多DI framework支援。另外則是不需要對建構式進行改變,或增加新的建構式。對過去已經存在的legacy code的影響,會比建構式的方式小一點點(但幾乎沒有太大差異)。

  2. 顧慮點:
    最常見的情況,就是使用目標物件時,相依介面應有其對應執行個體,但卻因為使用端沒有設定public property,導致使用方法時出現NullReferenceException,這種情況也怪不了使用端,因為使用端極有可能本就不瞭解這個方法中,有哪些相依物件。

解決方式與建構式的建議雷同,首先當然要有測試程式來說明(測試程式就是物件使用說明書),另外取得目標物件,仍可透過Repository Pattern,讓使用端無須瞭解目標物件的相依關係。

並且在方法中使用相依介面前,應檢查其是否為Null,若為Null,則代表參數設定錯誤,進行error handling,避免已經發生錯誤仍執行許多不應執行的程式碼。或是在property的getter時,檢查是否為null或當為null時,給予一預設值,以避免方法無法正常執行。(視實際需求而定)

另外,公開屬性的方式,也如同公開建構式一般,破壞了一點點物件封裝的用意。但這兩者,都是IoC設計會帶來的影響。

@呼叫方法時傳入參數

  1. 描述:
    既然前面兩種方式,都可能造成使用方法時,可能沒有設定好相依介面的執行個體,導致發生錯誤。或是使用目標物件時,不知道該呼叫哪一個建構式或初始化哪些屬性。那很簡單的方式,就是把方法相依介面的部分,拉到方法的參數上。方法中,需要使用到哪些介面,強迫由呼叫端必須給定參數。目標物件的方法內容則僅相依於參數上的介面。

程式碼如下:

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

            return passwordByDao == hashResult;
        }

2.好處:
不必再擔心要先初始化哪些property,或呼叫哪一個建構式。當要呼叫某一個方法,其相依的物件,就是得透過參數來給定。基本上也不太需要擔心使用上造成困擾或迷惑。

  1. 顧慮點:
    最大的問題,在於方法簽章上的不穩定性。當需求異動,該方法需要額外相依於其他物件時,方法簽章可能會被迫改變。而方法簽章是物件導向設計上,最需要穩定的條件之一。以物件導向、介面導向設計來說,當多型物件方法簽章不一致時,向來是個大問題。

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

而且透過參數的方式,DI framework支援度較低。

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

by the way, 這個方式是可以與其他方式共存的,所以在設計物件時,可衡量搭配使用。

@可覆寫的保護方法

  1. 描述:
    前面的三種方式,基本上都對外暴露了原本可能不需要對外暴露的細節。倘若,現在的需求是眼前的程式要進行測試,但又不希望影響或修改使用端的程式,那麼該怎麼作呢?除了可以透過公開屬性設定,當為空時給予預設值的方式,來維持原本物件的內部程式邏輯以外,還有一個相當簡單的方式,甚至有些情況不需要透過介面設計,就可以進行測試。

先來看看原本直接相依物件,無法測試的程式,程式碼如下:

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

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

不難,對吧。接下來,建立一個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。

偵錯測試一下,就可以確認,程式碼就跟之前使用IoC的方式執行沒有太大的差異。請見下圖:

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

  2. 顧慮點:
    這是為了測試,且legacy code所使用的方式,而不是良好的物件導向設計的方式。IoC的用意在於介面導向與擴充點的彈性,所以當可測試之後,倘若重構影響範圍不大,建議讀者朋友還是要將物件改相依於介面,透過IoC的方式來設計物件。

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

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

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

下一篇文章,將介紹怎麼樣可以避免每次手工刻這麼囉唆的stub物件,怎麼針對static或.net framework本身的物件進行隔離,怎麼針對物件與相依介面互動的情況進行測試。


上一篇
[Day 5]如何隔離相依性 - 基本的可測試性
下一篇
[Day 7]Unit Test - Stub, Mock, Fake簡介
系列文
30天快速上手TDD31

2 則留言

0
pajace2001
iT邦研究生 1 級 ‧ 2012-10-14 17:03:50

沙發
昨晚沒等到~
今天還是讓我做到沙發了~~~YA~
期待好久了開心開心

就是91 iT邦研究生 4 級‧ 2012-10-14 17:05:11 檢舉

請問一下,您是我plurk or Facebook上的好友嗎?

不然怎麼會這麼快就發現文章發出去了 驚

pajace2001 iT邦研究生 1 級‧ 2012-10-14 23:36:03 檢舉

嘿嘿因為假日不用上班~我一直守在電腦前等待你的文章阿偷笑
阿~偶像~偶像~開心

我要留言

立即登入留言