论程序员的自我修养——重构(1)

来源:互联网 发布:微信业务域名是什么 编辑:程序博客网 时间:2024/06/05 19:49

重构与我

        想写重构,完全是因为上个月看了一本关于重构的书,里面介绍了十几种不同的重构方法。真的是手把手形式的教你各种重构的方法,书名叫《重构:改善既有代码的设计》(http://book.douban.com/subject/1229923/)。不能说这本书能带给我多大的启发,里面介绍的大部分代码设计的方法,都是我已经学过或知道的,但这算是我第一次真正系统地接触重构。尽管以前一直有优化自己代码的习惯,但用系统的方法论重构代码还是在读了这本书以后的事情。
        然后重构慢慢成为了习惯,无论是在写WeiboSpider的过程中,还是在工作的项目中,删除代码对我来说成了一件很平常的事情。有时候会因为新增功能的之前重构,有时候会为了单元测试而重构。当然很多时候仅仅是默默地做,因为重构的动机更多时候是因为热情。对于混饭吃的程序员,我只要实现功能就行了,何必花时间排除代码的“臭味”,又不会加工资的;对于老板,他更关心的是项目收益和成本,重不重构程序功能都能实现,目的就已经达到了。就如我题目所言,重构是程序员的自我修养,只有对编程抱有热情,才有重构的动力。不过我也希望能把这种修养推广到身边的人,所以也就重构的主题在团队内部分享过一次,至于效果如何,就拭目以待吧。

重构与优化

        重构的概念其实并不复杂,就是在不改变程序行为的情况下改善程序。但要注意一点的是,重构并不等于优化。虽然都不能改变程序的输入上下文和输出结果,但重构的侧重点在于改善程序的可读性、扩展性和程序结构;优化的侧重点则在于改善程序的性能。就这点的不一样,导致了很多时候重构和优化是无法同时进行的。比如在重构的理念中,拆分函数是提倡的,小函数更有助于提高程序的可读性和重用性。但这个在优化上是不提倡的,因为函数调用时的上下文切换是会影响程序性能的(当然对于现代计算机和现代的编译器,这种性能影响是可以忽略不计的)。因此在修改代码的时候,应该在脑海中清晰知道,现在做的究竟是重构还是优化,这直接决定了程序应该改成什么样子的。

重构与单元测试

        有人说重构是一个永恒的话题,但对我来说单元测试才是一个永恒的话题。是的,重构依然能和单元测试沾上边。对于重构,有一个很重要的前提是不能改变程序输入的上下文,也不能改变程序的输出结果。很多时候重构无法进行,不是因为我们不知道这个程序写得不好,也不见得是因为懒,是因为害怕,害怕修改代码引入新的bug,谁希望自己千辛万苦修复好bug以后的程序,由于自己一个小小的改动,就无法运行了呢?
        因此这个时候需要单元测试。如果一个测试用例在没有被修改的前提下,重构前能运行通过,重构后却运行失败了,那么可以肯定这个被测程序的行为被改变了,证明这次重构失败了。运行单元测试的成本很低,按一下按钮或者输入一个命令,就可以全方位地检测程序是否按设定的行为运行了。速度也很快,只要程序不是特别复杂,几秒或者几十秒后就能知道这次的重构是否引入新bug了。
        单元测试是我们放胆修改程序的保证。程序员要做的最重要的事情之一就是用计算机防止和包容人的错误,单元测试就是防止我们在重构中犯错。

重构与代码可读性

        代码可读性一直是在程序设计中被反复强调但又很难解决的问题。因为可读性不仅受程序员水平的影响,也受程序员对业务理解的影响。比如变量的命名,不仅不同的人对命名的理解不一样,就算是同一个人,随着对业务理解的深入,命名风格可能也会发生变化。有的公司为了规范代码命名,会花很大力气,比如指定字典一般的命名规范,然后用各种code review或工具检验代码,不过都收效神威。
        重构为不断改善代码可读性提供了很高的工具,因为重构的目的之一就是要提升代码的可读性。或者刚开始不见得能对变量或函数起一个很好的名字,或许随着对业务的理解深入,真的觉得有必要修改某个变量的名字。不要紧,勇敢去改吧,对于需要编译的语言,一般编译器能帮助我们发现修改命名导致的错误;对于解析性的语言,现代IDE或文本编辑器能帮助我们很快地发现变量或函数被引用的地方,结合单元测试,修改命名并不会造成额外很多的工作量。
        代码可读性另外一个很重要的方面就是简短的代码块。试想一下,一个上百行的函数,写的时候的确很爽,但调试和为它写单元测试的时候,就会有想砸键盘的冲动了。对于一个长函数,必定包含大大小小的小逻辑,把这些小逻辑拆成小函数,不仅能帮助迅速定位问题调试,单元测试的时候也能针对较为简单的逻辑写单元测试,设计用例也没那么抓狂了。
        下面看一个例子。以下的代码是重构前的代码:
void PrintOWing(){    double outstanding = 0;    //print banner    System.Console.WriteLine("***********************");    System.Console.WriteLine("*******Custom Owe******");    System.Console.WriteLine("***********************");    //calculate outstanding    foreach (Order item in this.Orders)    {        outstanding += item.Amount;    }    //print details    System.Console.WriteLine("name:" + this.Name);    System.Console.WriteLine("amount" + outstanding);}
        以上的代码也算不上什么很复杂的长函数,但是也包含了三个小逻辑(以注释划分)。接下来可以看看这段代码是如何重构的:
void PrintOWing(){    PrintBanner();    PrintDetails(CalculateOutstanding());}void PrintBanner(){    System.Console.WriteLine("***********************");    System.Console.WriteLine("*******Custom Owe******");    System.Console.WriteLine("***********************");}void PrintDetails(double outstanding){    System.Console.WriteLine("name:" + this.Name);    System.Console.WriteLine("amount" + outstanding);}double CalculateOutstanding(){    double outstanding = 0;    foreach (Order item in this.Orders)    {        outstanding += item.Amount;    }    return outstanding;}
        可以看到,每个小逻辑都被拆分成小函数了。这里的重构也体现了一种编写代码的风格:用函数名代替注释。当一段逻辑复杂得需要需要用注释说明其作用的时候,就把这段逻辑提取成函数,然后用函数名来说明这段逻辑。拆分小函数还有另外一个显而易见的好处,就是提高代码的重用性,不知道以后什么时间,这些小函数就能被重用了。
        对于程序员来说一个很基本的技能便是懂得把大问题分解成小问题。或许第一次写这段代码的时候,还是写得过长了,通过重构,把长的代码块拆分成短代码块吧。