[探讨]通过实例再讨论TDD

ref

在《测试驱动开发》(Kent Beck)的附录B,Kent Beck用了两页纸的篇幅,演示了一次完全以测试驱动的方式,开发计算斐波纳契数列。

先简短的抄一下代码,再谈谈我的看法。

第一个测试与第一次的代码

代码
  1. publicvoidtestFibonacci()
  2. assertEquals(0,fib(0));
  3. }
  4. intfib(intn){
  5. return0;
  6. }

第二个测试与第二次的代码

代码
  1. publicvoidtestFibonacci()
  2. assertEquals(0,fib(0));
  3. assertEquals(1,fib(1));
  4. }
  5. intfib(intn){
  6. if(n==0)return0;
  7. return1;
  8. }

对测试代码进行改进,使之更为通用

代码
  1. publicvoidtestFibonacci(){
  2. intcases[][]={{0,0},{1,1}};
  3. for(inti=0;i<cases.length;i++){
  4. assertEquals(cases[i][1],fib(cases[i][0]));
  5. }

再增加n=2的测试

代码
  1. publicvoidtestFibonacci(){
  2. intcases[][]={{0,1},{2,fib(cases[i][0]));
  3. }

不需要修改代码,测试就通过了。

再增加n=3的测试

代码
  1. publicvoidtestFibonacci(){
  2. intcases[][]={{0,{3,2}};
  3. for(inti=0;i<cases.length;i++){
  4. assertEquals(cases[i][1],fib(cases[i][0]));
  5. }

测试失败,于是修改代码,还是如法炮制

代码
  1. intfib(intn){
  2. if(n==0)return0;
  3. if(n<=2)return1;
  4. return2;
  5. }

然后,最为神奇的部分在下面的四次修改:

1:

代码
  1. intfib(intn){
  2. if(n==0)return0;
  3. if(n<=2)return1;
  4. return1+1;//注意这里
  5. }

2:
代码
  1. intfib(intn){
  2. if(n==0)return0;
  3. if(n<=2)return1;
  4. returnfib(n-1)+1;//注意这里
  5. }

3:
代码
  1. intfib(intn){
  2. if(n==0)return0;
  3. if(n<=2)return1;
  4. returnfib(n-1)+fib(n-2);//注意这里
  5. }

4:
代码
  1. intfib(intn){
  2. if(n==0)return0;
  3. if(n==1)return1;//注意这里
  4. returnfib(n-1)+fib(n-1);
  5. }

这是一个非常棒的过程。我们的讨论也从这里开始。

最后得到的这个函数,是一个递归函数,非常的简洁,但是往往会有效率问题。

(打住,告诉过你多少次了,不要考虑效率!)

不是我要考虑效率,只是这么简单的例子,要寻找别的设计方式,我只能从效率方面来说事。

OK,继续。假设我们要求9的斐波纳契数列的值,那么,fib函数就会去计算fib(8 )+fib(7)。然后我们再展开。
fib(9)=fib(8 )+fib(7)
fib(9)=(fib(7)+fib(6))+(fib(6)+fib(5))
注意,这里fib(6)就要被计算两遍。
fib(9)=((fib(6)+fib(5))+(fib(5)+fib(4)))+((fib(5)+fib(4))+(fib(4)+fib(3)))
注意,这里fib(5)要被计算3遍,fib(4)要被计算3遍。

理解我的意思了吗?这样的算法,存在严重的效率隐患。
如果我们要考虑效率,会如何写代码呢?

代码
  1. publicintfib(intn){
  2. intvalue0=0;
  3. intvalue1=0;
  4. intvalue=0;
  5. for(inti=0;i<=n;i++){
  6. if(i==1){
  7. value1=1;
  8. value=1;
  9. }else{
  10. value=value0+value1;
  11. value0=value1;
  12. value1=value;
  13. }
  14. }
  15. returnvalue;
  16. }

这个算法我就不解释了。有人也许会说,你这样不是TDD,你先写了程序!

不要紧,我可以假装先写了测试代码

代码
  1. publicvoidtestFibonacci(){
  2. intcases[][]={{0,fib(cases[i][0]));
  3. }

然后再把刚才的那个程序写出来,这样有什么问题吗?这样还算是TDD吗?

我仔细看了书了,Kent Beck说过“步伐”问题。我这样也可以算是TDD的,只是步子大了点。

那么我想说明什么问题呢?
1、无论先写测试还是先写代码,都需要考虑设计问题
2、在写测试之前考虑设计问题,不是什么罪过
3、考虑设计思路的深入与否,决定了步伐的大小
4、步伐太小的设计考虑,可能会陷入死角,无法再优化下去。从上面的代码可以看到,要想使递归算法变成循环算法,不是重构能够做到的。

最终的结论是: 代码就像你的左脚,测试就像你的右脚。 你可以先迈左脚,再迈右脚。然后一直走下去。 也可以先迈右脚,再迈左脚。然后一直走下去。 只要你不是一直单脚跳着前进,你都会走得很稳,而且没有人看得出区别来。

相关文章

什么是设计模式一套被反复使用、多数人知晓的、经过分类编目...
单一职责原则定义(Single Responsibility Principle,SRP)...
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强...
适配器模式将一个类的接口转换成客户期望的另一个接口,使得...
策略模式定义了一系列算法族,并封装在类中,它们之间可以互...
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,...