TDD个人实践体会(C#)五

引言

  之前的blog - TDD个人实践体会(C#)一到四篇,主要是记录了我用TDD的方式编写一个排列组合的选择器,可以在一至四看到TDD如何影响设计、编码、实现阶段。

  这一篇blog主要是记录在TDD如何影响对选择器进行重构的过程。所以,我们用前四篇完成的代码作为我们开始的基础。

  可以 点击这里 下载基础源代码(当然也可以一步步的从一至四看到代码,我个人也建议这么做,虽说看起来蛮长的几篇,其实做完也不需要多少时间。)


目录

  1. 重新测试及检查代码
  2. 重构设计
  3. 重构测试代码
  4. 重构功能代码
  5. 小结

正文

1.重新测试及检查代码

既然用了TDD的方式,那么在编码的过程中,个人有个体会,就是可以很方便快速的对代码进行测试。这让我可以在任何时候,快速的检查个人的代码。

在编码之前,我和快速的Ctrl + R,Ctrl + A运行了一下测试代码。

测试:通过。

然后打开代码覆盖率结果的窗口查看一下测试代码是否全部覆盖到了代码。(这里补充一句,我原本应该在第四篇结束的时候就查看覆盖率的,但是上次我blog写到半夜两点,困得要命,盖上本子就睡了。)

 96.33 % ,这个结果让我感到很奇怪,测试代码覆盖率按照估计,应该100%的。

再来检查一下,没有覆盖到的几句代码:

 

在求组合的方法中:

private List<T[]> BuildComposeResult(T[] source,  int count)
        {
            List<T[]> result =  new List<T[]>();
             if (count ==  1)
            {
                 foreach (T item  in source) result.Add( new T[] { item });
                 return result;
            }
             if  (count == 0return 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 == nullreturn 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%

 

2.重构设计

关于重构,每个人有自己的想法,这里我将分支重构为对象行为,使用一个接口的不同实现,来对不同逻辑计算进行分支。

接口包含一个NewInstance的方法,来创建对应的实例对象。(其实最初我选择了使用抽象工厂来进行创建,后来又更改为使用创建实例的方法,来创建对象。这里你可以选择你自己认为更合适的方法来进行你自己的重构。)

重构后的设计(这里已经是重构阶段,所以,我们忽略了原始的设计)

 

Selector类内包含一个IFormula的属性,用来存储Selector将要采用的算法Formula。

Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。

Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。

IFormula的Calculate方法,接收一个Selector对象作为传入参数,计算结果将会存入给定Selector对象的Result内。
 
IFormula有两个实现类,ComposeFormula 和 PermetationFormula。
 
ComposeFormula 和 PermetationFormula 包含静态的NewInstance方法,用来构造Formula实例对象。
 
SelectType类不再有价值,应该删除。
 

 

3.重构测试代码

依据对设计的重构,修改测试代码(无法编译通过的)。

依据对设计的重构,可以看出,对Selector的外部行为的改变影响到的测试代码,主要包含了两部分:

  1)修改了Selector的构造函数,增加了传入参数 IFormula 。

  2)修改了DoProcess方法,去除了枚举参数。

根据设计变化修改后的测试代码 

 

DoSelectorTest()方法内:

        
            Selector<int> intSelector = new Selector<int>(intSource, intCountToSelect);
            intSelector.DoProcess(SelectType.Compose);
改为
            Selector< int> intSelector =  new Selector< int>(intSource, intCountToSelect, ComposeFormula.NewInstance());
            intSelector.DoProcess();
 
            Selector<int> intSelector2 = new Selector<int>(intSource, intCountToSelect2);
            intSelector2.DoProcess(SelectType.Compose);

改为
            Selector< int> intSelector2 =  new Selector< int>(intSource2, intCountToSelect2, ComposeFormula.NewInstance());
            intSelector2.DoProcess();
 
            Selector<object> objSelector = new Selector<object>(objSource, objCountToSelect, PermutationFormula.NewInstance());
            objSelector.DoProcess();

改为
            Selector< object> objSelector =  new Selector< object>(objSource, objCountToSelect, PermutationFormula.NewInstance());
            objSelector.DoProcess();

 

DoCreateSelectorTest()方法内主要测试在构造对象时,传入各种非法参数,是否正确抛出ApplicationException,也需要调整方法内的创建对象代码。

还需要新增一条对传入IFormula为null的检查代码

             try
            {
                Selector< object> selector =  new Selector< object>( new  object[] {  8127896 },  3null);
                Assert.Fail( " 没有抛Formula为空 ApplicationException ");
            }
             catch (ApplicationException) { }

 

CheckResultCount() 方法内的创建对象代码也需要调整。

全部调整完后,运行测试,一定会失败。

下一步就要一步步的调整功能代码,来使测试能够通过。

 

4.重构功能代码

依据对设计的重构来逐步对代码重构,用测试代码来验证重构结果。 

Selector类内包含一个IFormula的属性,用来存储Selector将要采用的算法Formula。
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
IFormula的Calculate方法,接收一个Selector对象作为传入参数,计算结果将会存入给定Selector对象的Result内。 
IFormula有两个实现类,ComposeFormula 和 PermetationFormula。 
ComposeFormula 和 PermetationFormula 包含静态的NewInstance方法,用来构造Formula实例对象。
SelectType类不再有价值,应该删除。 

 先看前两条,包含了下面的代码修正

  1)创建一个IFormula的Interface

  2)在Selector内创建一个Private的属性

  3)修改Selector的构造函数,添加IFormula参数

IFormula代码

namespace MathLibrary
{
     public  interface IFormula
    {
    }
}

 

Selector代码(斜体下划线为新增代码)

         private  IFormula Formula { getset; }
         public Selector(T[] sourceObjects, int countToSelect ,IFormula formula){
             if (sourceObjects ==  nullthrow  new ApplicationException( " 给定的sourceObjects不允许为null ");
             if (countToSelect <  1throw  new ApplicationException( " 给定的countToSelect不允许小于1 ");
             if (countToSelect > sourceObjects.Count())  throw  new ApplicationException( " 给定的countToSelect不允许大于sourceObjects包含的元素总数 ");
             if (HaveRepeatedObject(sourceObjects))  throw  new ApplicationException( " 给定的sourceObjects不允许包含重复元素 ");
             if  (formula == nullthrow new ApplicationException("给定的formula不允许为空");
             this.SourceObjects = sourceObjects;
             this.CountToSelect = countToSelect;
             this.Formula = formula;
        }

编译功能代码:通过

编译测试代码:失败

 

再反回来回顾一下设计,我们已经解决了设计中的前两条了。 

Selector类内包含一个IFormula的属性,用来存储Selector将要采用的算法Formula。
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。

Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
IFormula的Calculate方法,接收一个Selector对象作为传入参数,计算结果将会存入给定Selector对象的Result内。 
IFormula有两个实现类,ComposeFormula 和 PermetationFormula。 
ComposeFormula 和 PermetationFormula 包含静态的NewInstance方法,用来构造Formula实例对象。
SelectType类不再有价值,应该删除。 

 

再看第三、四条设计,包含了下面功能代码的修正

  1)IFormula包含一个Calclate方法,参数为Selector类型

  2)在Selector的DoProcess内调用IFormula对象的Calculate方法

 

     public  interface IFormula
    {
         void Calculate<T>(Selector<T> selector);
    }

 

 

Selector的DoProcess

         public  void DoProcess(SelectType selecType) {
             this.Formula.Calculate<T>( this);
        }

 

看到DoProcess的这句代码,让人感觉非常不好,this.Formula的Calculate,将this传入,最后结果又被赋值在this的Result内,这的确让人感觉非常的不好。

我们可以再任何认为合适的时候进行重构。

再次重构设计,只是做了轻微的修改: 

Selector类内包含一个IFormula的属性,用来存储Selector将要采用的算法Formula。
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
在Selector的构造函数内,将Selector对象本身注册给IFormula
IFormula包含一个RegistSelector方法,将使用它的Selector注册给其内部的Selector属性
Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
IFormula的Calculate方法,接收一个Selector对象作为传入参数,计算结果将会存入给定Selector对象的Result内。  
IFormula有两个实现类,ComposeFormula 和 PermetationFormula。 
ComposeFormula 和 PermetationFormula 包含静态的NewInstance方法,用来构造Formula实例对象。
SelectType类不再有价值,应该删除。 

 检查重构后的设计是否引起测试代码的变化,这里我们并不需要修改测试代码。

 修改的设计(蓝色字体),只需要修改Selector的构造函数和IFormula的代码即可

 

IFormula代码

namespace MathLibrary
{
     public  interface IFormula<T>
    {
        Selector<T> Selector { get; }
        void RegistSelector(Selector<T> selector);
        void Calculate();
    }
}

 

 Selector中的代码

         private IFormula<T> Formula { getset; }
         public Selector(T[] sourceObjects, int countToSelect ,IFormula<T> formula){
             if (sourceObjects ==  nullthrow  new ApplicationException( " 给定的sourceObjects不允许为null ");
             if (countToSelect <  1throw  new ApplicationException( " 给定的countToSelect不允许小于1 ");
             if (countToSelect > sourceObjects.Count())  throw  new ApplicationException( " 给定的countToSelect不允许大于sourceObjects包含的元素总数 ");
             if (HaveRepeatedObject(sourceObjects))  throw  new ApplicationException( " 给定的sourceObjects不允许包含重复元素 ");
             if (formula ==  nullthrow  new ApplicationException( " 给定的formula不允许为空 ");
             this.SourceObjects = sourceObjects;
             this.CountToSelect = countToSelect;
             this.Formula = formula;
             this.Formula.RegistSelector(this);
        }
         public  void DoProcess() {
             this.Formula.Calculate();
        }

 

这下重构后的代码就好看多了(当然你也可以选择你的重构方式)。

编译功能代码:通过

编译测试代码:失败

 

目前的设计

Selector类内包含一个IFormula的属性,用来存储Selector将要采用的算法Formula。
Selector类在创建对象的时候,必须给定IFormula的对象,IFormula不允许在外部对其修改。
在Selector的构造函数内,将Selector对象本身注册给IFormula
IFormula包含一个RegistSelector方法,将使用它的Selector注册给其内部的Selector属性
Selector类在DoProcess方法内调用IFormula对象的Calculate方法计算结果。
IFormula有两个实现类,ComposeFormula 和 PermetationFormula。 
ComposeFormula 和 PermutationFormula 包含静态的NewInstance方法,用来构造Formula实例对象。
SelectType类不再有价值,应该删除。 

 我们将没有实现的最后两个设计包含的项加入到代码中

  1)创建IFormula的两个实现类(目前里面的代码还是空的,将会在下一步将功能代码,向两个实现类迁移。)

  2)在实现类内增加静态NewInstance方法,返回对应的对象实例。

  3)删除SelectorType类

编译功能代码:通过

编译测试代码:失败

这里的测试代码按照预期,应该是会通过的,但是实际上却失败了,检查测试代码后,我们发现,IFormula的两个实现类,都包含了泛型,然而我们在最初写测试代码的时候,遗漏了这一点。调整测试代码后,让测试代码可以正常运行。

 

     public  class ComposeFormula<T>:IFormula<T>
    {
         public  static IFormula<T> NewInstance() {  return  new ComposeFormula<T>(); }
         public Selector<T> Selector {  getprivate  set; }
         public  void RegistSelector(Selector<T> selector){ }
         public  void Calculate(){ }
    }

 

     public  class PermutationFormula<T>:IFormula<T>
    {
         public  static IFormula<T> NewInstance() {  return  new PermutationFormula<T>(); }
         public Selector<T> Selector {  getprivate  set; }
         public  void RegistSelector(Selector<T> selector){ }
         public  void Calculate(){ }
    }

 

测试代码内需要把所有的

ComposeFormula.NewInstance() 修改为 ComposeFormula<对应的类型>.NewInstance()

PermutationFormula.NewInstance() 修改为 PermutationFormula<对应的类型>.NewInstance()

 

编译:通过

测试:失败

 

最后,我们进行算法代码的迁移工作,使测试代码可以通过测试。

编译:通过

测试:通过

 

我就是这么一步步的用TDD的方式构建了一个排列组合算法的功能。

当然这段代码还有很多的重构点,但是我并不想再继续写如何重构的内容,这篇blog主要是讲一下,如何用TDD来影响重构,而不是主讲重构。

你可以 点击这里 下载到目前位置的代码,然后使用TDD继续进行你自己的重构,来体会一下TDD如何影响你的编码。

 

5.小结

在这次使用TDD的过程中,我自己有了对TDD的直观的认识:

1.最初的确会让编码速度产出速度减低。

2.如果你的设计,很难编写测试代码,通常这个设计也会很难使用,通常暗示你需要重构你的设计。

3.对功能代码对外表现的改变,都会首先在测试代码内得到体现。

4.要重构功能代码,首先构建功能模块的单元测试代码,将会很有用(如果之前的测试代码完整归档,将会非常有用)。

5.测试代码和功能代码最好对应的保存,或者你有一个自己的机制,能让你很方便的找到自己的功能模块对应的测试代码。

6.如果重构一段代码,这段代码引起了测试代码的变化,你就需要检查整个系统内所有使用到这段代码的部分,这些代码也许需要做和测试代码一样的调整。

7.重构代码时候,如果完全没有调整测试代码,系统内那么使用这段代码的部分,通常也不需要调整。

8.以测试代码通过为原则,通常可以预防你对系统的过度设计。

转载于:https://www.cnblogs.com/daitou0322/archive/2012/06/12/TDD_CS_005.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值