重构是一种改善已有代码和设计的有效手段,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 类是这样的:
ReferenceFile 类的hibernate映射文件是这样的:
回头看看在这次功能调整中,我们需要做哪几项任务?其中会涉及哪些方面?
很明显,随着需求的变化,作为一组时时刻刻同时出现而且内聚性非常强的属性,原来记录文件相关信息的属性组,比如文件名、上传路径以及类型等等,以及操作 这些属性的方法需要抽取到一个单独的类里面。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。 这两个类的代码大概如下:
接下来,我们需要修改 ReferenceFile 的持久化机制。在原始的设计里面,ReferenceFile类的属性一一对应到数据库表中的字段。现在属性被分到了两个对象里面,为了 Hibernate依旧能把这些属性都持久化到一张数据库表里面,我们使用了 Hibernate 提供的 component配置。下面是改动后的配置:
运行测试,OK,所有的测试都pass了。至此,我们抽取新类的步骤就完成了。接下来,我们需要完成“一对多”的演化。
在这里面,我们需要将 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对象之后的结构:
这里,我们就考虑把Attachment单独持久化到自己的数据库表里面了。原来的component就变成了现在一对一关联。改动后的配置如下:
到这里,读者就能发现这是一种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.
那么,在本文的功能开发之中,我们是如何做到的?
到这里,“一对多”的工作完成之后,ReferenceFile 和 Attachment 类就变成了下文的样子:
为了能实现一对多的实体关系,我们需要引入新的表作为“多”方,并保持“一”方的主键。使用Hibernate提供的one-to-many很容易做到这点,接下来是简单的配置文件:
至此,我们就完成了新功能的开发,可以看出整个过程的思路非常明显,而且因为主要是沿着重构的思想一路下来,思路非常清晰。另外,因为重构已经有成熟的IDE支持,我们可以利用到IDE的很多便利,这从另一方面也给我们带来了非常的效率。
从整个过程来看,重构的一些方法和思想,不仅可以让我们对遗留代码进行优化,使之能有利于新功能的开发(比如本文中的抽取新类和公开新类,都是为了下文的 “由一到多”的功能开发),而且可以让我们在开发功能的时候能从一个更高的角度来分解功能的开发工作,从而把原本复杂无序的过程简化抽象成一段明确的重构 链。那么,重构是否就是开发人员开发软件的领域专属语言呢(refactoring as DSLs to developers' development)?敬请期待本博关于这点的其他博文。