关于重构
重构是一种改善已有代码和设计的有效手段,Martin Fowler的著作 Refactoring:Improving the Design of Existing Code一书里提出了若干种重构的模式,深刻地影响了众多的开发人员。但如果认为重构只能做到小范围的代码优化,或者设计优化,并视之为无法影响更高层面工作的雕虫小技,那就大错特错了。之后 Joshua Kerievsky 的著作 Refactoring to Patterns则创造性地把重构和模式联系在一起,让人们认识到重构的巨大威力。重构从来不是程序员躲在象牙塔孤芳自赏的技术,也可以对系统的设计开发发挥巨大的作用。如果说Martin的 Refactoring只是深刻地影响了普通开发人员的程序设计和代码编写,Joshua的 Refactoring to Patterns则切切实实地给架构设计人员或者tech lead的工作指出了革命性的变化。那么,对于项目的功能开发,重构又意味着什么呢?对于项目经理来讲,应用重构的技术和思想,对整个项目的功能开发是否能带来特别的好处?下面将通过一个例子给大家展示在开发新功能时,对开发的每一步都保持重构的思想,将整体功能的开发分解成若干步骤的“重构”,从而非常简易清晰地完成功能开发。
新功能的提出
某系统中存在 ReferenceFile 类,作为用户向系统上传的文件的抽象,是系统其他地方会使用到的参考文档。在最初的需求中,参考文档与用户上传的文件一一对应,并且用户能指定某些系统特定的属性,比如文档类别、文档管理部门等。于是在最初的设计中,该类的属性包含两部分:一部分是前面提到的系统属性;另一部分是描述文件信息的属性,比如文件名、文件存储路径等。请注意,这里我将“参考文档”与“上传文件”两个概念区分出来,是为了便于下文解释。总的来说,在这个阶段,“参考文档”就是系统对“上传文件”的抽象。接到这个需求之后,我们使用TDD,很快就驱动设计出该类的业务方法;再使用Acceptance TDD,又对该类的功能的进行了全面的覆盖;最后使用hibernate的O/R Mapping,按照属性和表字段一对一的关系,把该类和数据库的表关联起来。完成UI方面的设计,并把前后台整合在一起。系统上线试运行后用户认为这块很好地契合了需求。
但是,需求总是不断在变的。上线过程中,用户的上级部门提出参考文档应该可以对应到多个上传文件,系统其他地方使用时把其下所有上传的文件作为一个“参考文档”整体来对待。也就是说,对 ReferenceFile 类而言,其中的系统属性仍然是保持一份,但是上传文件的属性则变成多份。概括下来,客户提出的新需求如下:
1. 参考文档可以管理多个上传文件
2. 用户创建或者修改参考文档时,可以同时上传多个文件,并能对已上传的文件进行删除修改
3. 系统在其他地方仍然是针对参考文档来参考引用用户上传的文件
4. 参考文档的预览和展示需要调整成支持多个上传文件
实现过程
该系统是标准的j2ee web 分层系统,包括web UI、controller、service、domain model、dao这几层。本文的重点是如何应用重构开发功能,本文将着重关注于domain层的改动,会包括domain model API的改动,以及domain model 持久化机制的改动。其他层次,比如controller、service等,因为主要是作为domain model的消费者,主要是使用domain model 的public API,故放在一起作为整体来对待,下文将统一称为client 代码。至于最外层的 web UI层,则因为主要是根据系统功能提供交互上的操作和内容展现,而且大部分情况下也会有专门UI设计开发,本文就不涉及了。另外,系统还包括大量不同层次的测试代码,比如unit test、functional test、integration test和regression test等等。从另外一个角度,测试代码又可以分成2部分:text fixture和test case。test fixture主要是负责测试数据的准备,test case才是测试用例的实现代码。前面提到的测试,除了unit test之外都主要是基于 web UI 模拟用户使用系统功能,test case 主要是针对 web UI 来写,故对于这部分的测试而言,domain model 的修改主要会影响到测试数据的准备。而对于 unit test,又可以根据SUT的不同,分为几个部分:针对model的unit test、针对client(包括controller和service)的unit test。其中,针对model的unit test也只是model API的消费者,也可以视为domain model的client。针对controller和service的unit test,理论上也只针对于SUT的API,对model的API依赖也只是在test fixture那块。所以,根据我们的分析,我们知道测试代码可以简化成两部分,一部分是与controller/service类似的domain model的client,另一部分是使用domain model生成一组aggregation的test fixture。
综上所述,我们把整个功能实现过程中涉及的工作主要归类为:domain model API的改动、domain model持久化机制的修改、domain model client的修改,以及test fixture的修改。现在对于需要做什么事情,就变得清晰了。我们接下来对前面三项工作来分析。
面临的现状
仔细分析我们面临的情况:1. 文件的相关信息在原始的 ReferenceFile 类里面是作为一对一的属性组存在
2. ReferenceFile 类使用 Hibernate 进行属性字段一对一的持久化
3. ReferenceFile 类以及原功能有 unit test、dao test,以及functional test 覆盖
此时的 ReferenceFile 类是这样的:
public
class
ReferenceFile {
private String category;
private String fileName;
// 相应的 getter/setter,以及业务方法
}
ReferenceFile 类的hibernate映射文件是这样的:
private String category;
private String fileName;
// 相应的 getter/setter,以及业务方法
}
<
class
name
="ReferenceFile"
table
="referenceFiles"
>
< id />
< property name ="category" />
< property name ="fileName" />
//
</ class >
回头看看在这次功能调整中,我们需要做哪几项任务?其中会涉及哪些方面?
< id />
< property name ="category" />
< property name ="fileName" />
//
</ class >
- domain model 的修改
- domain model 持久化机制的修改
- domain model 增加一对多的关系
抽取新类
domain model 的修改
很明显,随着需求的变化,作为一组时时刻刻同时出现而且内聚性非常强的属性,原来记录文件相关信息的属性组,比如文件名、上传路径以及类型等等,以及操作这些属性的方法需要抽取到一个单独的类里面。Martin Fowler 在Refactoring:Improving the Design of Existing Code里面写到“...consider where it can be split...A good sign is that a subste of the data and a subset of the methods seem to go together.”因此,我们决定把这些属性组和方法抽取到一个新类。新的类的职责变成维护上传文件的相关信息,而 ReferenceFile 则化身为一组上传文件的集合,不用操心文件的存储和具体细节,更利于系统其他地方进行引用。那我们该如何进行演化呢?这里我们可以使用 Martin Fowler在Refactoring书中的“Extract Class”技巧。请大家自行参阅,就不具体讲了。经过这一步,我们现在可以得到这样一个结构:ReferenceFile has an Attachment。 这两个类的代码大概如下:
public
class
ReferenceFile {
private String category;
private Attachment attachment;
// 相应的 getter/setter,以及业务方法
}
public class Attachnment {
private String fileName;
// 相应的 getter/setter,以及业务方法
}
private String category;
private Attachment attachment;
// 相应的 getter/setter,以及业务方法
}
public class Attachnment {
private String fileName;
// 相应的 getter/setter,以及业务方法
}
domain model 持久化机制的修改
接下来,我们需要修改 ReferenceFile 的持久化机制。在原始的设计里面,ReferenceFile类的属性一一对应到数据库表中的字段。现在属性被分到了两个对象里面,为了 Hibernate依旧能把这些属性都持久化到一张数据库表里面,我们使用了 Hibernate 提供的 component配置。下面是改动后的配置:
<
class
name
="ReferenceFile"
table
="referenceFiles"
>
< id />
< property name ="category" />
< component class ="Attachment" >
< property name ="fileName" />
//
</ component >
</ class >
运行测试,OK,所有的测试都pass了。至此,我们抽取新类的步骤就完成了。接下来,我们需要完成“一对多”的演化。
< id />
< property name ="category" />
< component class ="Attachment" >
< property name ="fileName" />
//
</ component >
</ class >
公开新类
domain model 的修改
在这里面,我们需要将 ReferenceFile 类里面的 Attachment 类公布出来,直接在client code里面使用这个类。这样,原本属于 Attachment 类的方法就能彻底地从 ReferenceFile 类里面移走,ReferenceFile类只留下必要的业务方法和 Attachment 对象的getter/setter。Martin Fowler在Refactoring:Improving the Design of Existing Code里提到“move methods”,我们采用这种技巧,很容易地把原来与Attachment类相关的业务方法都移到Attachment类里面,ReferenceFile类里面只保留对attachment属性的getter/setter方法。公布Attachment对象之后的结构:
public
class
ReferenceFile {
private String category;
private Attachment attachment;
// 相应的 getter/setter,以及业务方法
}
public class Attachnment {
private Long id;
private String fileName;
// 相应的 getter/setter,以及业务方法
}
private String category;
private Attachment attachment;
// 相应的 getter/setter,以及业务方法
}
public class Attachnment {
private Long id;
private String fileName;
// 相应的 getter/setter,以及业务方法
}
domain model 持久化机制的修改
这里,我们就考虑把Attachment单独持久化到自己的数据库表里面了。原来的component就变成了现在一对一关联。改动后的配置如下:
<
class
name
="ReferenceFile"
table
="referenceFiles"
>
< id />
< property name ="category" />
< one-to-one name ="attachment" class ="Attachment" />
//
</ class >
< class name ="Attachment" table ="attachments" >
< id />
< property name ="fileName" />
</ class >
< id />
< property name ="category" />
< one-to-one name ="attachment" class ="Attachment" />
//
</ class >
< class name ="Attachment" table ="attachments" >
< id />
< property name ="fileName" />
</ class >
实现类之间的一对多联系
domain model 的修改
到这里,读者就能发现这是一种Kent Beck曾经总结过的“First One, Then Many”情况。关于“First One,Then Many”,Kent Beck曾写了一篇文章介绍如何可靠地拥抱变化,原文链接如下http://www.threeriversinstitute.org /FirstOneThenMany.html。在那篇文章中,Kent的问题是面对未来可能的需求变化,如何使用 Succession 的方式帮助系统架构平滑演化。下面是 Kent 的观点:Applied to software design, succession refers to creating a design in stages. The first stage is not where you expect to end up, but it provides value. You have to pay the price to progress from stage to stage, but if jumping to the final stage is prohibitively expensive in time or money, or if you don't know enough to design the "final" stage, or if (as postulated above) the customers don't know enough to specify the system that would use the final stage, then succession provides a disciplined, reasonably efficient way forward.
那么,在本文的功能开发之中,我们是如何做到的?
- 增加字段attachments,以及getter/setter
- 修改原来单个Attachment的getter/setter,改成从attachments里面得到首元素或者往里面添加新元素,如getAttachments().get(0)
- 运行测试,确保所有测试都通过
- inline ReferenceFile类里面的对单个Attachment的getter/setter方法。这里要注意test fixture里面对domain model的aggregation的创建,而且因为涉及对List的操作,所以可能需要修改原来的测试代码和test fixture
- 运行测试,确保所有测试都通过
到这里,“一对多”的工作完成之后,ReferenceFile 和 Attachment 类就变成了下文的样子:
public
class
ReferenceFile {
private String category;
private List<Attachment> attachments;
// 相应的 getter/setter,以及业务方法
}
public class Attachnment {
private Long id;
private String fileName;
// 相应的 getter/setter,以及业务方法
}
private String category;
private List<Attachment> attachments;
// 相应的 getter/setter,以及业务方法
}
public class Attachnment {
private Long id;
private String fileName;
// 相应的 getter/setter,以及业务方法
}
domain model 持久化机制的修改
为了能实现一对多的实体关系,我们需要引入新的表作为“多”方,并保持“一”方的主键。使用Hibernate提供的one-to-many很容易做到这点,接下来是简单的配置文件:
<
class
name
="ReferenceFile"
table
="referenceFiles"
>
< property name ="category" />
< set name ="attachments" cascade ="all" >
< key column ="id" />
< one-to-many class ="Attachment" />
</ set >
//
</ class >
< class name ="Attachment" table ="attachments" >
< property name ="fileName" />
</ class >
< property name ="category" />
< set name ="attachments" cascade ="all" >
< key column ="id" />
< one-to-many class ="Attachment" />
</ set >
//
</ class >
< class name ="Attachment" table ="attachments" >
< property name ="fileName" />
</ class >
结论
至此,我们就完成了新功能的开发,可以看出整个过程的思路非常明显,而且因为主要是沿着重构的思想一路下来,思路非常清晰。另外,因为重构已经有成熟的IDE支持,我们可以利用到IDE的很多便利,这从另一方面也给我们带来了非常的效率。从整个过程来看,重构的一些方法和思想,不仅可以让我们对遗留代码进行优化,使之能有利于新功能的开发(比如本文中的抽取新类和公开新类,都是为了下文的“由一到多”的功能开发),而且可以让我们在开发功能的时候能从一个更高的角度来分解功能的开发工作,从而把原本复杂无序的过程简化抽象成一段明确的重构链。那么,重构是否就是开发人员开发软件的领域专属语言呢(refactoring as DSLs to developers' development)?敬请期待本博关于这点的其他博文。