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


上一篇我们编写了第一个测试代码,我们再来看一下我们的 US, 测试备注 和 设计

 

US:我希望可以给定一个包含m元素对象的集合,给定我想从中选出的元素个数n,从中找出所有满足条件的元素组合列表(无序) C(n,m) 或 排列列表(有序) P(n,m)

 

设计

1、我需要创建一个类库,暂且命名为:MathLibrary
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()

 

 

 重构后的设计

1、我需要创建一个类库,暂且命名为:MathLibrary
2、类库内包含 ComposerSelector<T> 和 PermutationSelector<T> 两个类
3、 ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子类
4、Selector<T> 包含了 T[] SourceObjects 属性和  int CountToSelect 属性,来存储数据源和要从中取出的排列组合要包含的项目个数
5、 ComposerSelector<T> 和 PermutationSelector<T> 对象,可以调用DoProcess方法来进行排列组合的运算
6、Selector<T> 包含了 Result 属性,这个属性用来存储 DoProcess 方法运算后的结果值
7、Selector<T> 包含 ResultCount 属性,返回结果包含的记录条数

这些设计,已经改变了代码的一些外部行为,因此需要重构的测试代码。

 

ComposerTest 类

        [TestMethod]
         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 类

        [TestMethod]
         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  class ComposerSelector<T>
    {
         public ComposerSelector(T[] sourceObjects,  int countToSelect) { }
    }

 

 PremutationSelector类

     public  class PremutationSelector<T>
    {
         public PremutationSelector(T[] sourceObjects,  int countToSelect) { }
    }

编译成功

测试通过

然而,这时候的代码还远远没有达到设计的要求,接着就逐条解决

3.ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子类

这一条,并不不改变类的外部行为,根本不需要改变测试代码只需要创建一个基类并在子类中继承基类

我不希望能够直接创建Selector的对象,因此将Selector类标记为Abstract

修改后的 PremutationSelector类

     public  class PremutationSelector<T>:Selector<T>
    {
         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 新增代码

 

        [TestMethod]
         public  void ComposerPropertiesTest() {
             int[] intSource =  new  int[] {  0123456789 };
             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.69ftrue };
             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 T[] SourceObjects {  getprivate  set; }
         public  int CountToSelect {  getprivate  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> 中新增 

         public  abstract  void DoProcess();

 

ComposerSelector<T> 和 PermutationSelector<T>  中新增

public  override  void DoProcess()
        {
        }

编译通过

测试成功

 

这个时候,可以查看代码覆盖率,仍旧是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 类内

        [TestMethod]
         public  void ComposerPropertiesTest() {
             int[] intSource =  new  int[] {  0123456789 };
             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.69ftrue };
             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>  类中添加

public List<T[]> Result {  getprivate  set; }

 

编译:成功

测试会有两条失败信息:

未通过 ComposerPropertiesTest MathLibraryTest Assert.IsNotNull 失败。 
未通过 PremutationPropertiesTest MathLibraryTest Assert.IsNotNull 失败。 

这是并没有在DoProcess方法对结果进行赋值引发的测试失败。

这点我们在第二条已经考虑到了。

第二条并不改变测试代码,但是需要我们在DoProcess方法添加对Result的赋值来覆盖Result的set;

为了能够更好的测试,在DoProcess中,将SourceObjects存为Result中的一条结果,这样,就可以使用equals方法来测试Result是否被正确赋值

第三条无需多说吧

我们分别在ComposerSelector和PermutationSelector的DoProcess方法中加入伪实现的代码;

 

             this.Result =  new List<T[]>() {  this.SourceObjects };

 

编译:失败

一点失误,由于子类的Result属性继承于基类,而对基类Result的set方法的private造成了子类也无法对其赋值。

只需要将基类内Result的set方法设为protected即可

 

修改代码
编译:通过

测试:通过

 

1、我需要创建一个类库,暂且命名为:MathLibrary
2、类库内包含 ComposerSelector<T> 和 PermutationSelector<T> 两个类
3、ComposerSelector<T> 和 PermutationSelector<T> 是 Selector<T> 的子类
4、Selector<T> 包含了 T[] SourceObjects 属性和 int CountToSelect 属性,来存储数据源和要从中取出的排列组合要包含的项目个数
5、ComposerSelector<T> 和 PermutationSelector<T> 对象,可以调用DoProcess方法来进行排列组合的运算
6、Selector<T> 包含了 Result 属性,这个属性用来存储 DoProcess 方法运算后的结果值
7、Selector<T> 包含 ResultCount 属性,返回结果包含的记录条数
 
DoProcess的功能尚未真正实现。

 

============待续================

到了这里,测试代码可以通过运行,而且覆盖率也很高 100% ,但是的测试代码内充满了让人恼怒的 bad smell 代码。

下一步,我并没有立即编写算法的代码,而是对测试代码和重构进行了简单的重构

这里我和一个朋友产生了争论,至今没有结果

对方说,我应该尽快地实现功能,在功能实现后再来进行重构,否则,我可能会耗费大量的工作在重构上而延缓了功能实现的时间(我同意,这的确会使功能实现的时间延后)

而我认为,这个时候,已经可以明显确定的设计上的缺陷,此时重构的成本最小,如果在完全实现功能后再重构,那么重构的工作量将会很大。

关于重构的时机,希望各位能够给些意见。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM