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

ref 

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

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

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

代码
  1. public void testFibonacci()  
  2.     assertEquals(0,fib(0));  
  3. }  
  4.   
  5. int fib(int n){  
  6.     return 0;  
  7. }  

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

代码
  1. public void testFibonacci()  
  2.     assertEquals(0,fib(0));  
  3.     assertEquals(1,fib(1));  
  4. }  
  5.   
  6. int fib(int n){  
  7.     if(n==0return 0;  
  8.     return 1;  
  9. }  

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

代码
  1. public void testFibonacci(){  
  2.     int cases[][]={{0,0},{1,1}};  
  3.     for(int i=0;i<cases.length;i++){  
  4.         assertEquals(cases[i][1],fib(cases[i][0]));  
  5. }  

再增加n=2的测试

代码
  1. public void testFibonacci(){  
  2.     int cases[][]={{0,0},{1,1},{2,1}};  
  3.     for(int i=0;i<cases.length;i++){  
  4.         assertEquals(cases[i][1],fib(cases[i][0]));  
  5. }  

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

再增加n=3的测试

代码
  1. public void testFibonacci(){  
  2.     int cases[][]={{0,0},{1,1},{2,1},{3,2}};  
  3.     for(int i=0;i<cases.length;i++){  
  4.         assertEquals(cases[i][1],fib(cases[i][0]));  
  5. }  

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

代码
  1. int fib(int n){  
  2.     if(n==0return 0;  
  3.     if(n<=2return 1;  
  4.     return 2;  
  5. }  

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

1:

代码
  1. int fib(int n){  
  2.     if(n==0return 0;  
  3.     if(n<=2return 1;  
  4.     return 1+1;//注意这里  
  5. }  

2:
代码
  1. int fib(int n){  
  2.     if(n==0return 0;  
  3.     if(n<=2return 1;  
  4.     return fib(n-1)+1;//注意这里  
  5. }  

3:
代码
  1. int fib(int n){  
  2.     if(n==0return 0;  
  3.     if(n<=2return 1;  
  4.     return fib(n-1)+fib(n-2);//注意这里  
  5. }  

4:
代码
  1. int fib(int n){  
  2.     if(n==0return 0;  
  3.     if(n==1return 1;//注意这里  
  4.     return fib(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. public int fib(int n){  
  2.     int value0=0;  
  3.     int value1=0;  
  4.     int value=0;  
  5.     for(int i=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.     return value;  
  16. }  

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

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

代码
  1. public void testFibonacci(){  
  2.     int cases[][]={{0,0},{1,1},{2,1},{3,2}};  
  3.     for(int i=0;i<cases.length;i++){  
  4.         assertEquals(cases[i][1],fib(cases[i][0]));  
  5. }  

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

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

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

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

你可能感兴趣的:([探讨]通过实例再讨论TDD)