TDD个人实践体会(C#)五
引言
之前的blog - TDD个人实践体会(C#)一到四篇,主要是记录了我用TDD的方式编写一个排列组合的选择器,可以在一至四看到TDD如何影响设计、编码、实现阶段。
这一篇blog主要是记录在TDD如何影响对选择器进行重构的过程。所以,我们用前四篇完成的代码作为我们开始的基础。
可以 点击这里 下载基础源代码(当然也可以一步步的从一至四看到代码,我个人也建议这么做,虽说看起来蛮长的几篇,其实做完也不需要多少时间。)
目录
正文
既然用了TDD的方式,那么在编码的过程中,个人有个体会,就是可以很方便快速的对代码进行测试。这让我可以在任何时候,快速的检查个人的代码。
在编码之前,我和快速的Ctrl + R,Ctrl + A运行了一下测试代码。
测试:通过。
然后打开代码覆盖率结果的窗口查看一下测试代码是否全部覆盖到了代码。(这里补充一句,我原本应该在第四篇结束的时候就查看覆盖率的,但是上次我blog写到半夜两点,困得要命,盖上本子就睡了。)
96.33 % ,这个结果让我感到很奇怪,测试代码覆盖率按照估计,应该100%的。
再来检查一下,没有覆盖到的几句代码:
在求组合的方法中:
{
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;
}
检查一下,第一句 if(count==0) return null; 因为我们在构造函数的时候,做过了count <= 0 的判断,因此使用构造函数创建的对象,永远不会有count == 0 的情况出现。
也因为 count == 0的情况不会出现,因此 在递归中,也不会有 返回 null 的情况。
这种情况下,是否保留这两句,就仁者见仁智者见智了,我这里选择删除了这两句代码(既然可以预估永远到不了,我宁愿选择删除掉,保持覆盖率的100%;这就是重构的特点,你不用去担心对错,你可以再你觉得合适的时候选择合适的方案。也许将来方案不合适了,你只要改成合适的方案就可以,而不要去追究当初为什么选择此方案。)
求排列的计算,也有一样的两句代码,我选择了删除掉。
测试:通过,覆盖率 100%
关于重构,每个人有自己的想法,这里我将分支重构为对象行为,使用一个接口的不同实现,来对不同逻辑计算进行分支。
接口包含一个NewInstance的方法,来创建对应的实例对象。(其实最初我选择了使用抽象工厂来进行创建,后来又更改为使用创建实例的方法,来创建对象。这里你可以选择你自己认为更合适的方法来进行你自己的重构。)
重构后的设计(这里已经是重构阶段,所以,我们忽略了原始的设计)
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
IFormula的Calculate方法,接收一个Selector对象作为传入参数,计算结果将会存入给定Selector对象的Result内。
依据对设计的重构,修改测试代码(无法编译通过的)。
依据对设计的重构,可以看出,对Selector的外部行为的改变影响到的测试代码,主要包含了两部分:
1)修改了Selector的构造函数,增加了传入参数 IFormula 。
2)修改了DoProcess方法,去除了枚举参数。
根据设计变化修改后的测试代码
DoSelectorTest()方法内:
intSelector.DoProcess(SelectType.Compose);
Selector<int> intSelector = new Selector<int>(intSource, intCountToSelect, ComposeFormula.NewInstance());
intSelector.DoProcess();
intSelector2.DoProcess(SelectType.Compose);
Selector<int> intSelector2 = new Selector<int>(intSource2, intCountToSelect2, ComposeFormula.NewInstance());
intSelector2.DoProcess();
objSelector.DoProcess();
objSelector.DoProcess();
DoCreateSelectorTest()方法内主要测试在构造对象时,传入各种非法参数,是否正确抛出ApplicationException,也需要调整方法内的创建对象代码。
还需要新增一条对传入IFormula为null的检查代码
{
Selector<object> selector = new Selector<object>(new object[] { 8, 12, 7, 8, 9, 6 }, 3, null);
Assert.Fail("没有抛Formula为空 ApplicationException");
}
catch (ApplicationException) { }
CheckResultCount() 方法内的创建对象代码也需要调整。
全部调整完后,运行测试,一定会失败。
下一步就要一步步的调整功能代码,来使测试能够通过。
依据对设计的重构来逐步对代码重构,用测试代码来验证重构结果。
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
IFormula的Calculate方法,接收一个Selector对象作为传入参数,计算结果将会存入给定Selector对象的Result内。
先看前两条,包含了下面的代码修正
1)创建一个IFormula的Interface
2)在Selector内创建一个Private的属性
3)修改Selector的构造函数,添加IFormula参数
IFormula代码
{
public interface IFormula
{
}
}
Selector代码(斜体下划线为新增代码)
public Selector(T[] sourceObjects,int countToSelect,IFormula formula){
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不允许包含重复元素");
if (formula == null) throw new ApplicationException("给定的formula不允许为空");
this.SourceObjects = sourceObjects;
this.CountToSelect = countToSelect;
this.Formula = formula;
}
编译功能代码:通过
编译测试代码:失败
再反回来回顾一下设计,我们已经解决了设计中的前两条了。
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
IFormula的Calculate方法,接收一个Selector对象作为传入参数,计算结果将会存入给定Selector对象的Result内。
IFormula有两个实现类,ComposeFormula 和 PermetationFormula。
SelectType类不再有价值,应该删除。
再看第三、四条设计,包含了下面功能代码的修正
1)IFormula包含一个Calclate方法,参数为Selector类型
2)在Selector的DoProcess内调用IFormula对象的Calculate方法
{
void Calculate<T>(Selector<T> selector);
}
Selector的DoProcess
this.Formula.Calculate<T>(this);
}
看到DoProcess的这句代码,让人感觉非常不好,this.Formula的Calculate,将this传入,最后结果又被赋值在this的Result内,这的确让人感觉非常的不好。
我们可以再任何认为合适的时候进行重构。
再次重构设计,只是做了轻微的修改:
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
IFormula有两个实现类,ComposeFormula 和 PermetationFormula。
SelectType类不再有价值,应该删除。
检查重构后的设计是否引起测试代码的变化,这里我们并不需要修改测试代码。
修改的设计(蓝色字体),只需要修改Selector的构造函数和IFormula的代码即可
IFormula代码
{
public interface IFormula<T>
{
Selector<T> Selector { get; }
void RegistSelector(Selector<T> selector);
void Calculate();
}
}
Selector中的代码
public Selector(T[] sourceObjects,int countToSelect,IFormula<T> formula){
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不允许包含重复元素");
if (formula == null) throw new ApplicationException("给定的formula不允许为空");
this.SourceObjects = sourceObjects;
this.CountToSelect = countToSelect;
this.Formula = formula;
this.Formula.RegistSelector(this);
}
public void DoProcess() {
this.Formula.Calculate();
}
这下重构后的代码就好看多了(当然你也可以选择你的重构方式)。
编译功能代码:通过
编译测试代码:失败
目前的设计
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
SelectType类不再有价值,应该删除。
我们将没有实现的最后两个设计包含的项加入到代码中
1)创建IFormula的两个实现类(目前里面的代码还是空的,将会在下一步将功能代码,向两个实现类迁移。)
2)在实现类内增加静态NewInstance方法,返回对应的对象实例。
3)删除SelectorType类
编译功能代码:通过
编译测试代码:失败
这里的测试代码按照预期,应该是会通过的,但是实际上却失败了,检查测试代码后,我们发现,IFormula的两个实现类,都包含了泛型,然而我们在最初写测试代码的时候,遗漏了这一点。调整测试代码后,让测试代码可以正常运行。
{
public static IFormula<T> NewInstance() { return new ComposeFormula<T>(); }
public Selector<T> Selector { get; private set; }
public void RegistSelector(Selector<T> selector){ }
public void Calculate(){ }
}
{
public static IFormula<T> NewInstance() { return new PermutationFormula<T>(); }
public Selector<T> Selector { get; private set; }
public void RegistSelector(Selector<T> selector){ }
public void Calculate(){ }
}
测试代码内需要把所有的
ComposeFormula.NewInstance() 修改为 ComposeFormula<对应的类型>.NewInstance()
PermutationFormula.NewInstance() 修改为 PermutationFormula<对应的类型>.NewInstance()
编译:通过
测试:失败
最后,我们进行算法代码的迁移工作,使测试代码可以通过测试。
编译:通过
测试:通过
我就是这么一步步的用TDD的方式构建了一个排列组合算法的功能。
当然这段代码还有很多的重构点,但是我并不想再继续写如何重构的内容,这篇blog主要是讲一下,如何用TDD来影响重构,而不是主讲重构。
你可以 点击这里 下载到目前位置的代码,然后使用TDD继续进行你自己的重构,来体会一下TDD如何影响你的编码。
在这次使用TDD的过程中,我自己有了对TDD的直观的认识:
1.最初的确会让编码速度产出速度减低。
2.如果你的设计,很难编写测试代码,通常这个设计也会很难使用,通常暗示你需要重构你的设计。
3.对功能代码对外表现的改变,都会首先在测试代码内得到体现。
4.要重构功能代码,首先构建功能模块的单元测试代码,将会很有用(如果之前的测试代码完整归档,将会非常有用)。
5.测试代码和功能代码最好对应的保存,或者你有一个自己的机制,能让你很方便的找到自己的功能模块对应的测试代码。
6.如果重构一段代码,这段代码引起了测试代码的变化,你就需要检查整个系统内所有使用到这段代码的部分,这些代码也许需要做和测试代码一样的调整。
7.重构代码时候,如果完全没有调整测试代码,系统内那么使用这段代码的部分,通常也不需要调整。
8.以测试代码通过为原则,通常可以预防你对系统的过度设计。