在《测试驱动开发》(Kent Beck)的附录B,Kent Beck用了两页纸的篇幅,演示了一次完全以测试驱动的方式,开发计算斐波纳契数列。
先简短的抄一下代码,再谈谈我的看法。
第一个测试与第一次的代码
- public void testFibonacci()
- assertEquals(0,fib(0));
- }
- int fib(int n){
- return 0;
- }
第二个测试与第二次的代码
- public void testFibonacci()
- assertEquals(0,fib(0));
- assertEquals(1,fib(1));
- }
- int fib(int n){
- if(n==0) return 0;
- return 1;
- }
对测试代码进行改进,使之更为通用
- public void testFibonacci(){
- int cases[][]={{0,0},{1,1}};
- for(int i=0;i<cases.length;i++){
- assertEquals(cases[i][1],fib(cases[i][0]));
- }
再增加n=2的测试
- public void testFibonacci(){
- int cases[][]={{0,0},{1,1},{2,1}};
- for(int i=0;i<cases.length;i++){
- assertEquals(cases[i][1],fib(cases[i][0]));
- }
不需要修改代码,测试就通过了。
再增加n=3的测试
- public void testFibonacci(){
- int cases[][]={{0,0},{1,1},{2,1},{3,2}};
- for(int i=0;i<cases.length;i++){
- assertEquals(cases[i][1],fib(cases[i][0]));
- }
测试失败,于是修改代码,还是如法炮制
- int fib(int n){
- if(n==0) return 0;
- if(n<=2) return 1;
- return 2;
- }
然后,最为神奇的部分在下面的四次修改:
1:
- int fib(int n){
- if(n==0) return 0;
- if(n<=2) return 1;
- return 1+1;//注意这里
- }
2:
- int fib(int n){
- if(n==0) return 0;
- if(n<=2) return 1;
- return fib(n-1)+1;//注意这里
- }
3:
- int fib(int n){
- if(n==0) return 0;
- if(n<=2) return 1;
- return fib(n-1)+fib(n-2);//注意这里
- }
4:
- int fib(int n){
- if(n==0) return 0;
- if(n==1) return 1;//注意这里
- return fib(n-1)+fib(n-1);
- }
这是一个非常棒的过程。我们的讨论也从这里开始。
最后得到的这个函数,是一个递归函数,非常的简洁,但是往往会有效率问题。
(打住,告诉过你多少次了,不要考虑效率!)
不是我要考虑效率,只是这么简单的例子,要寻找别的设计方式,我只能从效率方面来说事。
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遍。
理解我的意思了吗?这样的算法,存在严重的效率隐患。
如果我们要考虑效率,会如何写代码呢?
- public int fib(int n){
- int value0=0;
- int value1=0;
- int value=0;
- for(int i=0;i<=n;i++){
- if(i==1){
- value1=1;
- value=1;
- } else {
- value=value0+value1;
- value0=value1;
- value1=value;
- }
- }
- return value;
- }
这个算法我就不解释了。有人也许会说,你这样不是TDD,你先写了程序!
不要紧,我可以假装先写了测试代码
- public void testFibonacci(){
- int cases[][]={{0,0},{1,1},{2,1},{3,2}};
- for(int i=0;i<cases.length;i++){
- assertEquals(cases[i][1],fib(cases[i][0]));
- }
然后再把刚才的那个程序写出来,这样有什么问题吗?这样还算是TDD吗?
我仔细看了书了,Kent Beck说过“步伐”问题。我这样也可以算是TDD的,只是步子大了点。
那么我想说明什么问题呢?
1、无论先写测试还是先写代码,都需要考虑设计问题
2、在写测试之前考虑设计问题,不是什么罪过
3、考虑设计思路的深入与否,决定了步伐的大小
4、步伐太小的设计考虑,可能会陷入死角,无法再优化下去。从上面的代码可以看到,要想使递归算法变成循环算法,不是重构能够做到的。
最终的结论是:
代码就像你的左脚,测试就像你的右脚。
你可以先迈左脚,再迈右脚。然后一直走下去。
也可以先迈右脚,再迈左脚。然后一直走下去。
只要你不是一直单脚跳着前进,你都会走得很稳,而且没有人看得出区别来。