您的位置:首页 > 编程语言 > C#

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

2012-06-06 18:18 288 查看
上一篇我们编写了第一个测试代码,我们再来看一下我们的 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[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
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.69f, true };
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 { get; private set; }
public int CountToSelect { get; private 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[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
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.69f, true };
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 { get; private 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 代码。

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

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

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

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

关于重构的时机,希望各位能够给些意见。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: