当我第一次知道“重构”这个词时,最直观的反应就是改代码,而且是有一定技巧的改代码。但上过邹欣老师的一节研究重构重要性的课(有一节是专门对重构的几个方面进行解读)和改过一次比较坑的java围棋代码后,我发现这是一项非常重要的技术。因为程序员写代码,虽然越来越趋于工程化,而且程序也经常要不同的人相互合作,而且同一个人也要改相当多次的代码来新增某一个目的。曹雪芹先生写《红楼梦》,“批阅十载,增删五次”,留下了恢弘巨著;要写出一个比较优美经典的程序,同样需要精雕细琢,提高其质量,这就是重构。
Martin Fowler首先给出了重构的定义:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。本质上说,重构就是“在代码写好之后改进它的设计”。重构是近几年来才在软件工程领域推出的一个方法论,其最初起源于极限编程(Extreme Programming)过程方法中,但很快就因其完善系统的理论原则和其对软件开发所带来的极大的利处而被其它的软件开发过程所采用,并得到了软件开发人员的认可,在软件工业界也得到了良好的应用。本书是为专业程序员写的重构指南,介绍了重构的一般性原则,以及一些重要的重构方法和准则。有的人可能会问,按照软件工程的思想,应该是先设计,后编码。为什么在编码之后,还要改变其设计?答案很简单:先前的设计存在问题,或是可读性较差,或是效率较差。通常是两者皆有。要注意的是,我们这里讲的,是详细设计,整体的框架设计,不在“重构”的讨论范围之内。重构的每个步骤都很简单,给人“小打小闹”的感觉,但聚沙成塔,这些小的改动,累加起来,可以从根本上改善设计和代码的质量。
虽然这本书是用Java语言描述的,但我想,使用任何语言的程序员,都能读懂这本书和其中的代码,如果感觉实在有困难,读一本最简单的JAVA的入门书即可。Eclipse SDK包括了一个全功能的Java IDE,并且有强大的机制支持代码的重构;但重构绝对不是限于JAVA的。在本书的13章,是伊利诺斯大学(University of Illinois)的博士,William Opdyke写的,其中引用了一篇C++程序重构的论文。另外,据说微软新出的Visual Studio Whidbey也会附带有重构的工具。
程序员,尤其是资历比较浅的还处于学生的我们都会有惰性。也不太明白设计的重要性,通常以完成功能为目的,写完的代码也不太多的必要改,因为只用一次就是交大作业,这促使我们重构的思想特别的薄弱。正常情况下,在水平提高后,程序员会对自己以前的代码非常看不顺眼,盼望能重新写好。可是,这是一项非常具有风险的活动。Martin Fowler的重构技术,它建立在完备的理论基础之上,并且有完善的方法学指导(包括小步迭代、频繁构建、测试优先等“敏捷联盟”倡导的实践),这使得它不再完全依赖于程序员的天赋。他在书中给出了具体的重构准则和严密的重构手法,使你能够在较低的风险下,重新设计,重写质量比较低劣的代码。
与重构紧密相连的另一个领域就是设计模式。虽然我们知道重构是为了使代码更优美,但怎样才算达到了重构的目标呢?设计模式为我们指明了方向。虽然设计模式是业界认可的良好的软件设计结构,但不可能从软件开发的开始阶段就把软件按设计模式全面地进行设计,那样只会带来是过分设计,最终浪费了大量的时间却无法获得良好的效果。而重构恰恰可以看作是软件设计的一个修正品,它可以在软件开发的过程中不断的修改现有的程序结构(即设计),而使软件的设计朝着设计模式的方向开去。这也正是重构的目标之所在。Joshua Kerievsky在《Refactoring to Patterns》一书中这样描述重构和模式的关系:Patterns are a cornerstone of object-oriented design, while test-first programming and merciless refactoring are cornerstones of evolutionary design(模式是面向对象设计的基石,而测试优先编程和无情的重构则是设计演进的基石)。
有一点要注意的是:“不改变代码外在行为的前提下”,通俗的说,就是不能改变这段代码实现的功能。这是一个非常重要,又常会有人违反的原则。如果要增加或减少功能,请在重构完成之后!因为重构的目标之一,就是保持代码的功能完全不变。这是本书在最后一章,重构技术的顶尖大师,Kent Beck一再强调的。
讲到这里,可能大家慢慢能理解,重构,最关注的,就是提高软件的质量。提高软件的质量,同时也就提高了开发速度。有些人可能会不理解:要是边写代码,边重构,当然比直接写出完成功能的代码慢呀。可是请你想一想,软件开发中最占用时间的是什么?测试,对,当然是测试。在debug的时候,是对着一堆很差劲,很难懂的代码舒服,还是对着已经重构过,可读性明显提升的代码好?可能在debug的时候,你还是难以忍受那些拙劣的代码,还是会去重构的。所以,在一开始写的时候,带着“重构”的思想去写,或是在写了一小段时间后,回头看一下自己的代码,“勿以善小而不为”,那不会浪费你的时间,从整体上来说,一定会大大节省整个项目的开发时间。
下面重点讲一讲对重构中一些章节的理解:
一.代码精短
这是我自己总结的词汇,主要的意思是当你的代码中有很深的嵌套条件时,花括号就会在代码中形成一个长长的箭头。我们经常在不同的代码中看到这种情况,并且这种情况也会扰乱代码的可读性。下代码所示,HasAccess方法里面包含一些嵌套条件,如果再加一些条件或者增加复杂度,那么代码就很可能出现几个问题:1,可读性差 2,很容易出现异常 3,性能较差
附上重构前的代码
using System.Collections.Generic; using System.Linq; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; using Customer = LosTechies.DaysOfRefactoring.BreakResponsibilities.After.Customer; namespace LosTechies.DaysOfRefactoring.SampleCode.ReturnASAP.Before { public class Order { public Customer Customer { get; private set; } public decimal CalculateOrder(Customer customer, IEnumerable<Product> products, decimal discounts) { Customer = customer; decimal orderTotal = 0m; if (products.Count() > 0) { orderTotal = products.Sum(p => p.Price); if (discounts > 0) { orderTotal -= discounts; } } return orderTotal; } } }
那么重构上面的代码也很简单,如果有可能的话,尽量将条件判断从方法中移除,我们让代码在做处理任务之前先检查条件,如果条件不满足就尽快返回,不继续执行。
下面是重构后的代码:
using System.Collections.Generic; using System.Linq; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; using Customer = LosTechies.DaysOfRefactoring.BreakResponsibilities.After.Customer; namespace LosTechies.DaysOfRefactoring.SampleCode.ReturnASAP.After { public class Order { public Customer Customer { get; private set; } public decimal CalculateOrder(Customer customer, IEnumerable<Product> products, decimal discounts) { if (products.Count() == 0) return 0; Customer = customer; decimal orderTotal = products.Sum(p => p.Price); if (discounts == 0) return orderTotal; orderTotal -= discounts; return orderTotal; } } }
二.合并相同功能的代码
改章的核心思想是把一些很多地方都用到的逻辑提炼出来,然后提供给调用者统一调用。
using System; namespace LosTechies.DaysOfRefactoring.RemoveDuplication.Before { public class MedicalRecord { public DateTime DateArchived { get; private set; } public bool Archived { get; private set; } public void ArchiveRecord() { Archived = true; DateArchived = DateTime.Now; } public void CloseRecord() { Archived = true; DateArchived = DateTime.Now; } } }
在上面的代码中我们发现ArchiveRecord和CloseRecord都会用到Archived = true; 和DateArchived = DateTime.Now; 这两条语句,所以我们就可以对它进行重构。重构后的代码如下
using System; namespace LosTechies.DaysOfRefactoring.RemoveDuplication.After { public class MedicalRecord { public DateTime DateArchived { get; private set; } public bool Archived { get; private set; } public void ArchiveRecord() { SwitchToArchived(); } public void CloseRecord() { SwitchToArchived(); } private void SwitchToArchived() { Archived = true; DateArchived = DateTime.Now; } } }
我提炼了SwitchToArchived方法来封装公用的操作,然后给ArchiveRecord和CloseRecord统一调用。
三.统一确定基类的方法
如标题所说,在这一章我主要学到如何将一个很多继承类都要用到的方法提升到基类中。具体来说是指将一个很多继承类都要用到的方法提升到基类中,这样就能减少代码量,同时让类的结构更清晰。如下代码所示,Turn方法在子类Car 和Motorcycle 都会用到,因为Vehicle 都会有这个方法,所以我们就会想到把它提到基类中。
namespace LosTechies.DaysOfRefactoring.PullUpMethod.Before { public abstract class Vehicle { // other methods } public class Car : Vehicle { public void Turn(Direction direction) { // code here } } public class Motorcycle : Vehicle { } public enum Direction { Left, Right } }
重构后的代码如下,那么现在Car 和Motorcycle 都具有Turn这个方法,如果这个方法修改也只需要修改基类即可,所以给维护和以后的重构带来了方便。
namespace LosTechies.DaysOfRefactoring.PullUpMethod.After { public abstract class Vehicle { public void Turn(Direction direction) { // code here } } public class Car : Vehicle { } public class Motorcycle : Vehicle { } public enum Direction { Left, Right } }
四.分解基类的方法
这一章讲的内容与上一张的思路相反,指的是把个别子类使用到的方法从基类移到子类里面去。如下代码所示,Animal 类中的方法Bark只有在其子类Dog 中使用,所以最好的方案就是把这个方法移到子类Dog中。
namespace LosTechies.DaysOfRefactoring.PushDownMethod.Before { public abstract class Animal { public void Bark() { // code to bark } } public class Dog : Animal { } public class Cat : Animal { } }
重构后的代码如下,同时如果在父类Animal 中如果没有其他的字段或者公用方法的话,可以考虑把Bark方法做成一个接口,从而去掉Animal 类。这里为了方便比较,使读者更容易明白分解的道理,还是把Animal类保留。
namespace LosTechies.DaysOfRefactoring.PushDownMethod.After { public abstract class Animal { } public class Dog : Animal { public void Bark() { // code to bark } } public class Cat : Animal { }
这本书是值得精读的,对于一个有开发经验的程序员,可能有部分是你已经知道并在开发过程中会注意的,别的部分,也会觉得很有道理而很容易的就记住了。所以,也许你读完一遍之后将不会再读第二遍,但却会时时刻刻想起它,因为它已经潜移默化了你的习惯。这让我想到了武侠书上说的“无招胜有招”,掌握了“有招”后,更深入的,就是“无招”,用来指导你写代码时的一切思想。正如作者所说,这些重构原则,不可能是全部,但如果你能由这些原则,悟到了如何写高质量代码的“道”,你就成为了一个真正的优秀的程序员了!