上一篇我們編寫了第一個測試代碼,我們再來看一下我們的 US, 測試備注 和 設計
US:我希望可以給定一個包含m元素對象的集合,給定我想從中選出的元素個數n,從中找出所有滿足條件的元素組合列表(無序) C(n,m) 或 排列列表(有序) P(n,m)
設計
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()
重構后的設計
2、類庫內包含 ComposerSelector<T> 和 PermutationSelector<T> 兩個類
這些設計,已經改變了代碼的一些外部行為,因此需要重構的測試代碼。
ComposerTest 類
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 類
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 ComposerSelector(T[] sourceObjects, int countToSelect) { }
}
PremutationSelector類
{
public PremutationSelector(T[] sourceObjects, int countToSelect) { }
}
編譯成功
測試通過
然而,這時候的代碼還遠遠沒有達到設計的要求,接着就逐條解決
3.ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子類
這一條,並不不改變類的外部行為,根本不需要改變測試代碼只需要創建一個基類並在子類中繼承基類
我不希望能夠直接創建Selector的對象,因此將Selector類標記為Abstract
修改后的 PremutationSelector類
{
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 新增代碼
public void ComposerPropertiesTest() {
int[] intSource = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
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.69f, true };
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 int CountToSelect { get; private 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> 中新增
ComposerSelector<T> 和 PermutationSelector<T> 中新增
{
}
編譯通過
測試成功
這個時候,可以查看代碼覆蓋率,仍舊是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 類內
public void ComposerPropertiesTest() {
int[] intSource = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
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.69f, true };
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> 類中添加
編譯:成功
測試會有兩條失敗信息:
未通過 ComposerPropertiesTest MathLibraryTest Assert.IsNotNull 失敗。
未通過 PremutationPropertiesTest MathLibraryTest Assert.IsNotNull 失敗。
這是並沒有在DoProcess方法對結果進行賦值引發的測試失敗。
這點我們在第二條已經考慮到了。
第二條並不改變測試代碼,但是需要我們在DoProcess方法添加對Result的賦值來覆蓋Result的set;
為了能夠更好的測試,在DoProcess中,將SourceObjects存為Result中的一條結果,這樣,就可以使用equals方法來測試Result是否被正確賦值
第三條無需多說吧
我們分別在ComposerSelector和PermutationSelector的DoProcess方法中加入偽實現的代碼;
編譯:失敗
一點失誤,由於子類的Result屬性繼承於基類,而對基類Result的set方法的private造成了子類也無法對其賦值。
只需要將基類內Result的set方法設為protected即可
修改代碼
編譯:通過
測試:通過
2、類庫內包含 ComposerSelector<T> 和 PermutationSelector<T> 兩個類
============待續================
到了這里,測試代碼可以通過運行,而且覆蓋率也很高 100% ,但是的測試代碼內充滿了讓人惱怒的 bad smell 代碼。
下一步,我並沒有立即編寫算法的代碼,而是對測試代碼和重構進行了簡單的重構
這里我和一個朋友產生了爭論,至今沒有結果
對方說,我應該盡快地實現功能,在功能實現后再來進行重構,否則,我可能會耗費大量的工作在重構上而延緩了功能實現的時間(我同意,這的確會使功能實現的時間延后)
而我認為,這個時候,已經可以明顯確定的設計上的缺陷,此時重構的成本最小,如果在完全實現功能后再重構,那么重構的工作量將會很大。
關於重構的時機,希望各位能夠給些意見。