在之前的实践中,主要说的是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如何对重构进行支援。