在之前的實踐中,主要說的是TDD過程如何影響對功能的設計,在這一篇,會開始實現組合和排列的算法,進而討論一下,TDD是如何的影響對實際功能代碼塊的影響的。
這里不再列舉之前的設計相關的列表,轉而專注於算法的實現,希望大家在這里先不要糾結於算法效率,畢竟這里只是對TDD進行討論,而不是算法專題。
好了,閑話少說,轉入正題
在之前的測試代碼中有這么一段
int intCountToSelect = 4;
Selector< int> intSelector = new Selector< int>(intSource, intCountToSelect);
intSelector.DoProcess(SelectType.Compose);
Assert.AreEqual(intSelector.SourceObjects, intSource);
Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
Assert.IsNull(intSelector.Result);
Assert.AreEqual(intSelector.ResultCount, 0);
這段代碼主要測試了在進行了對組合操作和相關屬性是否符合我們預期的測試,只是並沒有真的對算法結果進行測試,下面就開始設計算法。
這里我選擇了使用遞歸來進行組合運算,大概可以這樣描述
如果 N == 1;
則將 M 個元素,每個元素作為一個單項存入結果列表並結果列表
for(i = 0; i < N ; i++){
從數據中取出第i個元素
將第i個元素后面的元素作為剩余元素
再遞歸從剩下的M-i-1個元素中取出 N - 1 個元素
將第i個元素和上條遞歸結果組織成本層結果項存入結果列表
}
返回結果列表
先思考一下這個算法蘊含的含義以及其對測試的影響。
假設,要從M個不重復元素中,取出N個元素的組合,M > N > 0
簡單的一句話,居然找到了了4個要測試的點(也許還有遺漏)
來看一下測試代碼該如何編寫
{
Selector< object> selector = new Selector< object>( null, 5);
Assert.Fail( " 沒有拋出源為null的ApplicationException ");
}
catch (ApplicationException) { }
try
{
Selector< object> selector = new Selector< object>( new object[] { 8, 9, 6 }, 5);
Assert.Fail( " 沒有拋CountToSelect大於源的長度的ApplicationException ");
}
catch (ApplicationException) { }
try
{
Selector< object> selector = new Selector< object>( new object[] { 8, 9, 6 }, 0);
Assert.Fail( " 沒有拋CountToSelect < 1 的 ApplicationException ");
}
catch (ApplicationException) { }
try
{
Selector< object> selector = new Selector< object>( new object[] { 8, 12, 7, 8, 9, 6 }, 3);
Assert.Fail( " 沒有拋存在重復數據的 ApplicationException ");
}
catch (ApplicationException) { }
Selector< object> selectorSucceed = new Selector< object>( new object[] { 8, 9, 6 }, 1);
}
我們為上面測試中的每一條構建了一個測試,並且在最后附加了一條符合要求的創建對象代碼
編譯:通過
測試:未通過 DoCreateSelectorTest MathLibraryTest Assert.Fail 失敗。沒有拋出源為null的ApplicationException
我自己是逐條的進行測試,編碼 -> 測試,編碼;這樣逐條的解決了這些未通過測試(我建議你也這么做),不過,這里請允許偷下懶,直接貼出整塊的代碼。
if (sourceObjects == null) throw new ApplicationException( " 給定的sourceObjects不允許為null ");
if (countToSelect < 1) throw new ApplicationException( " 給定的countToSelect不允許小於1 ");
if (countToSelect > sourceObjects.Count()) throw new ApplicationException( " 給定的countToSelect不允許大於sourceObjects包含的元素總數 ");
if (HaveRepeatedObject(sourceObjects)) throw new ApplicationException( " 給定的sourceObjects不允許包含重復元素 ");
this.SourceObjects = sourceObjects;
this.CountToSelect = countToSelect;
}
private bool HaveRepeatedObject(T[] source) {
return source.Distinct().Count() < source.Count();
}
編譯:通過
測試:通過
如果 N == 1;
則將 M 個元素,每個元素作為一個單項存入結果列表並結果列表
for(i = 0; i < N ; i++){
從數據中取出第i個元素
將第i個元素后面的元素作為剩余元素
再遞歸從剩下的M-i-1個元素中取出 N - 1 個元素
將第i個元素和上條遞歸結果組織成本層結果項存入結果列表
}
返回結果列表
這一段,完整的描述了一個遞歸求組合的算法,可以根據這個算法推算出一些簡單的輸入和輸出,可以預期
如果輸入
int[] intSource = new int[] { 0, 1, 2, 3 }
countToSelect = 3
DoProgress 后結果會是 { new int[] { 0, 1, 2 }, new int[] { 0, 1, 3 }, new int[] { 0, 2, 3 }, new int[] { 1, 2, 3 } }
這里我突然意識到,我最初設計,如果Result是null的話,ResultCount結果是 0 ,這是一個非常低級的錯誤
因為這樣的話我在ResultCount 是0的情況下,根本無法區分是因為 DoProgress 沒有運行 而 Result 是null,還是 DoProgress 失敗,還是真的運算后的結果條數是 0;
因此,我又補充了一條測試:
如果 Result 是 null,讀取 ResultCount 屬性時,拋出 Resut 為 null 的 ApplicationException
於是我又分離出了一個對ResultCount屬性讀操作的測試
public void CheckResultCount()
{
try
{
Selector< object> selector = new Selector< object>( new object[] { 8, 12, 7, 9, 6 }, 3);
int count = selector.ResultCount;
Assert.Fail( " 沒有拋出Result為null,無法獲取ResultCount的ApplicationException ");
}
catch (ApplicationException) { }
}
測試:未通過 CheckResultCount MathLibraryTest Assert.Fail 失敗。沒有拋出Result為null,無法獲取ResultCount的ApplicationException
修正ResultCount屬性的代碼來使這個測試通過
get {
if ( this.Result == null) throw new ApplicationException( " Result為null,無法獲取ResultCount的值,可能是沒有進行運算或者運算失敗 ");
return this.Result.Count;
}
}
測試:未通過 DoSelectorTest MathLibraryTest 測試方法 MathLibraryTest.SelectorTest.DoSelectorTest 引發了異常:…………
新構建的測試方法通過了,但是舊的測試方法卻失敗了。
查看一下,發現原來是最初模擬實現了如果做Compose,Result的值為null的假設引起的。
修正DoProcess代碼,將Compose后的Result也做和Premutation一樣的設定
同時還需要將
Assert.IsNull(intSelector.Result);
改為
Assert.IsNotNull(intSelector.Result);
編譯:通過
測試:通過
這個時候,終於可以集中精力編寫進行Compose的代碼了
先重構一下測試代碼
舊代碼
int intCountToSelect = 4;
Selector< int> intSelector = new Selector< int>(intSource, intCountToSelect);
intSelector.DoProcess(SelectType.Compose);
Assert.AreEqual(intSelector.SourceObjects, intSource);
Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
Assert.IsNotNull(intSelector.Result);
Assert.AreEqual(intSelector.ResultCount, 0);
根據剛剛對結果進行預期重構的新代碼
int intCountToSelect = 3;
List< int[]> intResult = new List< int[]>() { new int[] { 0, 1, 2 }, new int[] { 0, 1, 3 }, new int[] { 0, 2, 3 }, new int[] { 1, 2, 3 } };
Selector< int> intSelector = new Selector< int>(intSource, intCountToSelect);
intSelector.DoProcess(SelectType.Compose);
Assert.AreEqual(intSelector.SourceObjects, intSource);
Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
Assert.IsTrue(intSelector.Result.Equals(intResult));
Assert.AreEqual(intSelector.ResultCount, 4);
編譯:成功
測試:未通過 DoSelectorTest MathLibraryTest Assert.IsTrue 失敗。
推算是因為並沒有真的實現DoProcess中Compose的計算,完成算法的代碼
{
List<T[]> result = new List<T[]>();
if (count == 1)
{
foreach (T item in source) result.Add( new T[]{ item });
return result;
}
if (count == 0) return null;
for ( int i = 1; i <= source.Count(); i++)
{
T selectedItem = source.Skip(i - 1).FirstOrDefault();
List<T[]> subResult = BuildComposeResult(source.Skip(i).Take(source.Count() - i).ToArray(), count - 1);
if (subResult == null) return null;
T[] tmp;
foreach (T[] item in subResult)
{
tmp = new T[count];
tmp[ 0] = selectedItem;
item.CopyTo(tmp, 1);
result.Add(tmp);
}
}
return result;
}
DoProcess方法內的代碼
this.Result = BuildComposeResult( this.SourceObjects, this.CountToSelect);
編譯:成功
測試:未通過 DoSelectorTest MathLibraryTest Assert.IsTrue 失敗。
這里我添加了斷點,發現輸出和預期是相同的,但是卻測試不通過,是 Equals 方法的錯誤,兩個完全不同的List<T[]>,即使內部值相同,hashcode也不同,因此Equals也會是false
我這里並不想重寫Equals或者Hashcode,我選擇了編寫了
bool EqualsResult<T>(IEnumerable<IEnumerable<T>> obj1, IEnumerable<IEnumerable<T>> obj2)
方法來輔助測試
(這個輔助測試的方法,我仍舊使用了TDD的方式來編寫,這里略過編寫過程,因為我並不想在將來對其重構,也許我將來根本不會用到這個方法;但是,如果真的出現了與這個類似的方法被多次使用,我們就需要考慮將其納入重構的項了。)
[TestMethod]
public void CheckEquals()
{
List<object[]> a = new List<object[]>() { new object[] { 0, "as", 36.8f }, new object[] { 10, "asasd", 5.6f } };
List<object[]> b = new List<object[]>() { new object[] { 0, "as", 36.8f }, new object[] { 10, "asasd", 5.6f } };
Assert.IsTrue(EqualsResult<object>(a, b));
}
{
if (obj1.Count() != obj2.Count()) return false;
var tmp1 = obj1.ToArray();
var tmp2 = obj2.ToArray();
for ( int i = 0; i < tmp1.Count(); i++) {
var count = ( from x in tmp1[i]
join y in tmp2[i]
on x equals y
select true).Count();
if (count != tmp1[i].Count()) return false;
}
return true;
}
將原本失敗的斷言修改為:
編譯:成功
測試:通過
雖說通過了這個測試,但是還是多編寫一些測試更保險,補充后的的測試
public void DoSelectorTest()
{
int[] intSource = new int[] { 0, 1, 2, 3 };
int intCountToSelect = 3;
List< int[]> intResult = new List< int[]>() { new int[] { 0, 1, 2 }, new int[] { 0, 1, 3 }, new int[] { 0, 2, 3 }, new int[] { 1, 2, 3 } };
Selector< int> intSelector = new Selector< int>(intSource, intCountToSelect);
intSelector.DoProcess(SelectType.Compose);
Assert.AreEqual(intSelector.SourceObjects, intSource);
Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
Assert.IsTrue(EqualsResult< int>(intSelector.Result,intResult));
Assert.AreEqual(intSelector.ResultCount, 4);
int[] intSource2 = new int[] { 0, 1, 2, 5 };
int intCountToSelect2 = 2;
List< int[]> intResult2 = new List< int[]>() { new int[] { 0, 1 }, new int[] { 0, 2 }, new int[] { 0, 5 }, new int[] { 1, 2 }, new int[] { 1, 5 }, new int[] { 2, 5 } };
Selector< int> intSelector2 = new Selector< int>(intSource2, intCountToSelect2);
intSelector2.DoProcess(SelectType.Compose);
Assert.AreEqual(intSelector2.SourceObjects, intSource2);
Assert.AreEqual(intSelector2.CountToSelect, intCountToSelect2);
Assert.IsTrue(EqualsResult< int>(intSelector2.Result, intResult2));
Assert.AreEqual(intSelector2.ResultCount, 6);
object[] objSource = new object[] { 10, ' A ', " HelloWorld ", 2.69f, true };
int objCountToSelect = 3;
Selector< object> objSelector = new Selector< object>(objSource, objCountToSelect);
objSelector.DoProcess(SelectType.Permutation);
Assert.AreEqual(objSelector.SourceObjects, objSource);
Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
Assert.IsNotNull(objSelector.Result);
Assert.AreEqual(objSelector.ResultCount, 1);
}
測試:通過
==============================================
到目前為止,我們使用TDD的方式,一步步完成了對Compose算法實現的編碼,我后面將會略過Premutation算法實現的過程
只給出包含Premutation部分的測試代碼
public void DoSelectorTest()
{
int[] intSource = new int[] { 0, 1, 2, 3 };
int intCountToSelect = 3;
List< int[]> intResult = new List< int[]>() { new int[] { 0, 1, 2 }, new int[] { 0, 1, 3 }, new int[] { 0, 2, 3 }, new int[] { 1, 2, 3 } };
Selector< int> intSelector = new Selector< int>(intSource, intCountToSelect);
intSelector.DoProcess(SelectType.Compose);
Assert.AreEqual(intSelector.SourceObjects, intSource);
Assert.AreEqual(intSelector.CountToSelect, intCountToSelect);
Assert.IsTrue(EqualsResult< int>(intSelector.Result,intResult));
Assert.AreEqual(intSelector.ResultCount, 4);
int[] intSource2 = new int[] { 0, 1, 2, 5 };
int intCountToSelect2 = 2;
List< int[]> intResult2 = new List< int[]>() { new int[] { 0, 1 }, new int[] { 0, 2 }, new int[] { 0, 5 }, new int[] { 1, 2 }, new int[] { 1, 5 }, new int[] { 2, 5 } };
Selector< int> intSelector2 = new Selector< int>(intSource2, intCountToSelect2);
intSelector2.DoProcess(SelectType.Compose);
Assert.AreEqual(intSelector2.SourceObjects, intSource2);
Assert.AreEqual(intSelector2.CountToSelect, intCountToSelect2);
Assert.IsTrue(EqualsResult< int>(intSelector2.Result, intResult2));
Assert.AreEqual(intSelector2.ResultCount, 6);
object[] objSource = new object[] { 10, ' A ', " HelloWorld " };
List< object[]> objResult = new List< object[]>() { new object[] { 10, ' A ' }, new object[] { 10, " HelloWorld " }, new object[] { ' A ', 10 }, new object[] { ' A ', " HelloWorld " }, new object[] { " HelloWorld ", 10 }, new object[] { " HelloWorld ", ' A ' } };
int objCountToSelect = 2;
Selector< object> objSelector = new Selector< object>(objSource, objCountToSelect);
objSelector.DoProcess(SelectType.Permutation);
Assert.AreEqual(objSelector.SourceObjects, objSource);
Assert.AreEqual(objSelector.CountToSelect, objCountToSelect);
Assert.IsTrue(EqualsResult< object>(objSelector.Result, objResult));
Assert.AreEqual(objSelector.ResultCount, 6);
}
在完成這個階段的編寫之后,我又對這個Selector進行了重構,我們后面繼續聊一下,在重構的過程中,TDD如何對重構進行支援。