TDD個人實踐體會(C#)二


上一篇我們編寫了第一個測試代碼,我們再來看一下我們的 US, 測試備注 和 設計

 

US:我希望可以給定一個包含m元素對象的集合,給定我想從中選出的元素個數n,從中找出所有滿足條件的元素組合列表(無序) C(n,m) 或 排列列表(有序) P(n,m)

 

設計

1、我需要創建一個類庫,暫且命名為:MathLibrary
2、類庫內包含 ComposerSelector<T> 和 PermutationSelector<T> 兩個類

3、兩個類都包含了 T[] SourceObjects 屬性和  int CountToSelect 屬性,來存儲數據源和要從中取出的排列組合要包含的項目個數
4、我希望在給定這些值后,可以調用DoProcess方法來進行排列組合的運算
5、我希望運算成功后,可以在對象的Result屬性讀取到對應的結果

 從之前的設計中,可以看到,ComposerSelector 和 PermutationSelector 有完全相同的屬性和方法,我們就要考慮采用什么形式進行抽象

SourceObject 、 CountToSelect 和 Result 屬性,在兩個類中會具有完全相同的特性、可訪問性和具體含義,顯然如果采用接口抽象並不合適,將其放在一個Selector<T>的基類中會更合適,也許需要重構一下設計了

需要增加一個Selector的基類

SourceObjects和CountToSelect屬性應該在賦值之后不允許外部對其進行修改,應該是pirvate set的

SourceObjects和CountToSelect需要創建能夠傳入對應參數的構造函數來對其進行初始的賦值

Result屬性石DoProcess計算的結果,也不允許在外部進行修改,也應該是 private set 的

希望在Selector后能有個屬性,直接獲取到返回的排列組合的條數,而不需要去再每次寫 selector.Result.Coun()

 

 

 重構后的設計

1、我需要創建一個類庫,暫且命名為:MathLibrary
2、類庫內包含 ComposerSelector<T> 和 PermutationSelector<T> 兩個類
3、 ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子類
4、Selector<T> 包含了 T[] SourceObjects 屬性和  int CountToSelect 屬性,來存儲數據源和要從中取出的排列組合要包含的項目個數
5、 ComposerSelector<T> 和 PermutationSelector<T> 對象,可以調用DoProcess方法來進行排列組合的運算
6、Selector<T> 包含了 Result 屬性,這個屬性用來存儲 DoProcess 方法運算后的結果值
7、Selector<T> 包含 ResultCount 屬性,返回結果包含的記錄條數

這些設計,已經改變了代碼的一些外部行為,因此需要重構的測試代碼。

 

ComposerTest 類

        [TestMethod]
         public  void TestCreateComposerSelector()
        {
            ComposerSelector<GenericParameterHelper> generalSelector = TestCreateComposerSelector<GenericParameterHelper>( new GenericParameterHelper[] { },  5);
            ComposerSelector< int> intSelector = TestCreateComposerSelector< int>( new  int[] { },  10);
            ComposerSelector< string> stringSelector = TestCreateComposerSelector< string>( new  string[] { },  3);
            ComposerSelector< object> objectSelector = TestCreateComposerSelector< object>( new  object[] { },  9);
        }

         private ComposerSelector<T> TestCreateComposerSelector<T>(T[] source,  int countToSelect)
        {
             return  new ComposerSelector<T>(source, countToSelect);
        

 

PremutationTest 類

        [TestMethod]
         public  void TestCreateComposerSelector()
        {
            ComposerSelector<GenericParameterHelper> generalSelector = TestCreateComposerSelector<GenericParameterHelper>( new GenericParameterHelper[] { },  5);
            ComposerSelector< int> intSelector = TestCreateComposerSelector< int>( new  int[] { },  10);
            ComposerSelector< string> stringSelector = TestCreateComposerSelector< string>( new  string[] { },  3);
            ComposerSelector< object> objectSelector = TestCreateComposerSelector< object>( new  object[] { },  9);
        }

         private ComposerSelector<T> TestCreateComposerSelector<T>(T[] source,  int countToSelect)
        {
             return  new ComposerSelector<T>(source, countToSelect);
        

 

編譯:失敗(這是肯定的)

我們下面就來調整代碼使創建對象的測試能夠通過

ComposerSelector類

 

     public  class ComposerSelector<T>
    {
         public ComposerSelector(T[] sourceObjects,  int countToSelect) { }
    }

 

 PremutationSelector類

     public  class PremutationSelector<T>
    {
         public PremutationSelector(T[] sourceObjects,  int countToSelect) { }
    }

編譯成功

測試通過

然而,這時候的代碼還遠遠沒有達到設計的要求,接着就逐條解決

3.ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子類

這一條,並不不改變類的外部行為,根本不需要改變測試代碼只需要創建一個基類並在子類中繼承基類

我不希望能夠直接創建Selector的對象,因此將Selector類標記為Abstract

修改后的 PremutationSelector類

     public  class PremutationSelector<T>:Selector<T>
    {
         public PremutationSelector(T[] sourceObjects,  int countToSelect): base(sourceObjects,countToSelect) { }
    }

PremutationSelector類就不給出代碼了,你可能又發現了可以重構的地方了吧,不過當時我並沒有發現這個,我是在后來代碼有了更多的行為后,才發現了需要做構造方法的重構的,所以,這里我並不解決它,會在后面對其進行重構,在早起,過多的應用重構還有過度使用設計模式,也許會讓你陷入一個泥潭。要知道,我們現在連一行具有實際意義的行為代碼都沒寫。

 

編譯成功

測試通過

 

下一條設計

4.Selector<T> 包含了 T[] SourceObjects 屬性和 int CountToSelect 屬性,來存儲數據源和要從中取出的排列組合要包含的項目個數

這一條,就明顯的需要來構建測試代碼來進行測試了,這兩個屬性,都有讀寫的操作

屬性寫操作將會在構造函數內被執行

而讀操作則需要我們進行讀取操作來覆蓋,下面就來編寫測試代碼

模擬從給定0-9十個數字中選出4個元素的Selector(ComposerSelector和PremutationSelector)

為了測試我們的設計能否應對更復雜的情況,我們再創建一個從一個包含有整型數字、字符、字符串、浮點、bool等不同類型對象的object的數組中選出三個元素的Selector(ComposerSelector和PremutationSelector)

ComposerTest 新增代碼

 

        [TestMethod]
         public  void ComposerPropertiesTest() {
             int[] intSource =  new  int[] {  0123456789 };
             int intCountToSelect =  4;
            ComposerSelector< int> intSelector =  new ComposerSelector< int>(intSource, intCountToSelect);
            Assert.AreEqual(intSelector.SourceObjects, intSource);
            Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);


             object[] objSource =  new  object[] {  10' A '" HelloWorld "2.69ftrue };
             int objCountToSelect =  3;
            ComposerSelector< object> objSelector =  new ComposerSelector< object>(objSource, objCountToSelect);
            Assert.AreEqual(objSelector.SourceObjects, objSource);
            Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
        }

 

編譯:失敗

下面要做的就是在Selector<T>類中增加對應的屬性,並在構造函數中增加賦值行為,來使測試可以通過。

 

Select<T> 類:

         public T[] SourceObjects {  getprivate  set; }
         public  int CountToSelect {  getprivate  set; }
         public Selector(T[] sourceObjects, int countToSelect){
             this.SourceObjects = sourceObjects;
             this.CountToSelect = countToSelect;
        }

編譯:成功

測試:通過

雖說這個時候代碼覆蓋率已經100%了(因為PremutationSelector的構造行為和這兩個屬性的相關操作在基類中已經被完成,因此已經被ComposerSelector的測試代碼覆蓋) 

我仍舊固執的為PremutationSelector編寫了測試代碼(這讓人很痛苦,一模一樣的代碼的復制黏貼,但這會更增加對其重構的決心。如果對重構抱有偏執,恭喜,這就是重構的原因之一,寫測試代碼遇到的讓人蛋疼的事情,在編寫功能代碼的時候,幾乎會100%遇到。)

 其實,這個問題,早在編寫構造函數的測試代碼時候,就碰到了,但我仍舊把對代碼的重構,放在了完成我們對修改的設計進行完整的編碼后才進行的。

繼續

 

 5.ComposerSelector<T> 和 PermutationSelector<T> 對象,可以調用DoProcess方法來進行排列組合的運算

目前為止對DoProcess的行為,還是只定義為依據給定的數據,進行運算,並且把結果存入Result屬性。

只需修改測試,調用DoProcess方法即可,可以發現,原來測試方法中的ComposerPropertiesTest和PremutationTest方法中已經做過了創建的操作,因此,只需要修改這個測試方法,加入調用方法即可。

 編譯:失敗

目前的類中還沒有DoProcess方法,肯定無法編譯通過,只要簡單的加入一個DoProcess方法就可以使其通過運行了。

目前 ComposerSelector<T> 和 PermutationSelector<T> 在DoProcess 方法上就有了差別,一個是求組合,一個是求排列。

DoProcess會在所有的Selector中都需要有的方法,將其放在抽象基類中,然后在子類對其實現。

 

 

Selector<T> 中新增 

         public  abstract  void DoProcess();

 

ComposerSelector<T> 和 PermutationSelector<T>  中新增

public  override  void DoProcess()
        {
        }

編譯通過

測試成功

 

這個時候,可以查看代碼覆蓋率,仍舊是100%,但是,目前還不能夠在測試代碼中對各個對象的DoProcess進行Assert來確定其是否在正確的位置被正確的執行,目前還不去具體實現算法,因此這個問題后面再解決。

 

6、Selector<T> 包含了 Result 屬性,這個屬性用來存儲 DoProcess 方法運算后的結果值

初步一看,這一條包含了兩條信息在里面

一,Selector<T> 包含了 Result 屬性

二,DoProcess 方法運算后會將結果存入Result

再分析一下,Result 應該是只能通過調用DoProcess方法來改變其值,對外部只讀。(當然這里因為我們缺少用戶方,也可以說我自己就是用戶,因此,我可以自己按自己的想法來設定。如果在正式工作中,應該與用戶溝通以確定各個細節的對外特性)

三,Result 的 set 方法應該是 private

繼續分析,

ComposerSelector<T> 和 PermutationSelector<T> 在DoProcess方法執行之后,Result 的值會不同,也有一些情況會出現相同的值的情況,視SourceObjects和CountToSelect值而定。

依據我們的上面三條分析結果

一,Selector<T> 包含了 Result 屬性

二,DoProcess 方法運算后會將結果存入Result

三,Result 的 set 方法應該是 private

 

只要對給對應的selector加入Result 的讀取操作就可以了,而Result的寫操作會在DoProcess被覆蓋。

在ComposerTest和PermutationTest測試方法內增加對DoProcess方法的調用和對Result方法的讀取

ComposerTest 類內

        [TestMethod]
         public  void ComposerPropertiesTest() {
             int[] intSource =  new  int[] {  0123456789 };
             int intCountToSelect =  4;
            ComposerSelector< int> intSelector =  new ComposerSelector< int>(intSource, intCountToSelect);
             intSelector.DoProcess();
            Assert.AreEqual(intSelector.SourceObjects, intSource);
            Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
             Assert.IsNotNull(intSelector.Result);


             object[] objSource =  new  object[] {  10' A '" HelloWorld "2.69ftrue };
             int objCountToSelect =  3;
            ComposerSelector< object> objSelector =  new ComposerSelector< object>(objSource, objCountToSelect);
             objSelector.DoProcess();
            Assert.AreEqual(objSelector.SourceObjects, objSource);
            Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
             Assert.IsNotNull(objSelector.Result);
        }

 

PermutationTest內代碼相同(我現在有了更多的重復代碼,你應該已經嗅出了bad smell了,重構的理由更加充分)

編譯:失敗

在Selector<T>中添加Result屬性即可; 

Selector<T>  類中添加

public List<T[]> Result {  getprivate  set; }

 

編譯:成功

測試會有兩條失敗信息:

未通過 ComposerPropertiesTest MathLibraryTest Assert.IsNotNull 失敗。 
未通過 PremutationPropertiesTest MathLibraryTest Assert.IsNotNull 失敗。 

這是並沒有在DoProcess方法對結果進行賦值引發的測試失敗。

這點我們在第二條已經考慮到了。

第二條並不改變測試代碼,但是需要我們在DoProcess方法添加對Result的賦值來覆蓋Result的set;

為了能夠更好的測試,在DoProcess中,將SourceObjects存為Result中的一條結果,這樣,就可以使用equals方法來測試Result是否被正確賦值

第三條無需多說吧

我們分別在ComposerSelector和PermutationSelector的DoProcess方法中加入偽實現的代碼;

 

             this.Result =  new List<T[]>() {  this.SourceObjects };

 

編譯:失敗

一點失誤,由於子類的Result屬性繼承於基類,而對基類Result的set方法的private造成了子類也無法對其賦值。

只需要將基類內Result的set方法設為protected即可

 

修改代碼
編譯:通過

測試:通過

 

1、我需要創建一個類庫,暫且命名為:MathLibrary
2、類庫內包含 ComposerSelector<T> 和 PermutationSelector<T> 兩個類
3、ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子類
4、Selector<T> 包含了 T[] SourceObjects 屬性和 int CountToSelect 屬性,來存儲數據源和要從中取出的排列組合要包含的項目個數
5、ComposerSelector<T> 和 PermutationSelector<T> 對象,可以調用DoProcess方法來進行排列組合的運算
6、Selector<T> 包含了 Result 屬性,這個屬性用來存儲 DoProcess 方法運算后的結果值
7、Selector<T> 包含 ResultCount 屬性,返回結果包含的記錄條數
 
DoProcess的功能尚未真正實現。

 

============待續================

到了這里,測試代碼可以通過運行,而且覆蓋率也很高 100% ,但是的測試代碼內充滿了讓人惱怒的 bad smell 代碼。

下一步,我並沒有立即編寫算法的代碼,而是對測試代碼和重構進行了簡單的重構

這里我和一個朋友產生了爭論,至今沒有結果

對方說,我應該盡快地實現功能,在功能實現后再來進行重構,否則,我可能會耗費大量的工作在重構上而延緩了功能實現的時間(我同意,這的確會使功能實現的時間延后)

而我認為,這個時候,已經可以明顯確定的設計上的缺陷,此時重構的成本最小,如果在完全實現功能后再重構,那么重構的工作量將會很大。

關於重構的時機,希望各位能夠給些意見。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM