Tag: OO

一次 Refactoring to Functionality 的实践

关于重构

重构是一种改善已有代码和设计的有效手段,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映射文件是这样的:

<class name=”ReferenceFile” table=”referenceFiles”>
<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,以及业务方法
}

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了。至此,我们抽取新类的步骤就完成了。接下来,我们需要完成“一对多”的演化。

公开新类
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,以及业务方法
}

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>
实现类之间的一对多联系
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.

那么,在本文的功能开发之中,我们是如何做到的?

  1. 增加字段attachments,以及getter/setter
  2. 修改原来单个Attachment的getter/setter,改成从attachments里面得到首元素或者往里面添加新元素,如getAttachments().get(0)
  3. 运行测试,确保所有测试都通过
  4. inline ReferenceFile类里面的对单个Attachment的getter/setter方法。这里要注意test fixture里面对domain model的aggregation的创建,而且因为涉及对List的操作,所以可能需要修改原来的测试代码和test fixture
  5. 运行测试,确保所有测试都通过

到这里,“一对多”的工作完成之后,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,以及业务方法
}

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>

结论

至此,我们就完成了新功能的开发,可以看出整个过程的思路非常明显,而且因为主要是沿着重构的思想一路下来,思路非常清晰。另外,因为重构已经有成熟的IDE支持,我们可以利用到IDE的很多便利,这从另一方面也给我们带来了非常的效率。

从整个过程来看,重构的一些方法和思想,不仅可以让我们对遗留代码进行优化,使之能有利于新功能的开发(比如本文中的抽取新类和公开新类,都是为了下文的“由一到多”的功能开发),而且可以让我们在开发功能的时候能从一个更高的角度来分解功能的开发工作,从而把原本复杂无序的过程简化抽象成一段明确的重构链。那么,重构是否就是开发人员开发软件的领域专属语言呢(refactoring as DSLs to developers’ development)?敬请期待本博关于这点的其他博文。

Beyond OSworkflow

最近结束了一个企业OA系统的项目,客户是一家海洋航运行业的企业,散运业务全球第一。该系统以工作流系统为基础平台,对员工工作进行电子化和规范化,由系统来驱动员工自动、快捷、可管地完成日常工作。在开发过程中,在综合评比多种工作流产品之后,我们选择了开源工作流产品——OSworkflow作为底层工作流引擎支撑,通过扩展OSworkflow的接口把业务系统和工作流引擎完美无缝地集成在一起。本文就是跟大家一起分享osworkflow扩展过程中的经验心得,希望能对其他朋友有帮助。

在我们开发团队介入系统开发过程之前,这个项目已经完成了 quickstart 阶段,留给我们的阶段产品有:系统实现的proposal、一个能体现用户page flow的lo-fi,以及在mingle上的 story list和iteration原始计划。当然,还有项目团队跟客户形成的一个良好的互信基础和沟通渠道。于是,左 proposal,右 lo-fi,客户沟通在中间,我们就开始了系统设计的 inception。inception阶段的目标根据不同项目会有不同。在这个系统里面,我们完成的目标有:确定系统实现使用的技术、针对技术进行spike探讨可行性、结合master story以及比较粗略的story list实现一个系统prototype。好,工作开始了!

为什么选择OSworkflow?
工作流系统其实在理论界已经是研究得非常成熟了,有 WfMC 组织来规范工作流定义语言,而且各种工作流引擎,不仅商业产品,像 JBPM、OSworkflow、ofbiz等开源产品也都很多。这些产品都各有特点,应该如何选择呢?从使用率和文档完整度来看,JBPM 和 OSworkflow 占了上风,我们就从这两者里面选了。JBPM基于UML的状态图和活动图来定义流程,已经加入JBOSS大家庭,但是相对比较重。osworkflow是个非常轻量级的工作流产品,但是自从2006年就停止活动了。不管如何,选择的技术都是要为解决未来系统开发会面临的问题,才算是一个好的可行的选择。

通过分析quickstart阶段的proposal和lo-fi prototype,然后再反复跟客户确认,我们知道了客户在现阶段对系统的要求是:

  1. 只需要处理线性的工作流(未来可能需要增加对分支/合并的支持)
  2. 不需要功能复杂强大的工作流编辑器
  3. 节点流转主要是手工触发,不需要基于规则
  4. 需要能和遗留系统已有的工作流定义集成,而已有工作流定义用 osworkflow 定义

根据客户对系统的要求,我们选择了osworkflow作为最终的候选工作流引擎。因为我们对这几种工作流产品都不是很熟,对它们是否能满足系统要求答案不确定。而从另外一个方面来讲,如果系统足够简单,我们也可以自己实现一个状态机,自己实现对流程节点流转的维护。

为了做出选择,我们又分头对 osworkflow 的配置文件以及技术架构都进行了细致的研究。研究过程和结果就不细说了,网上有很多 OSworkflow 的介绍文章,大家可以参照。OSworkflow 会把一个简单的xml配置文件,转换成它定义的流程描述类(XXXDescriptor,我们也把它们称为osworkflow的models),然后在流转的过程中,将其转换成工作流实例和工作流节点,并通过相应的持久化接口持久化工作流的状态。整个过程用一个简图来表示,就会是:

图1 OSworkflow model

考虑到 OSworkflow 的实现也是非常轻量级,并且结构非常清晰,而且为了提供以后对分支/合并、以及自定义规则等流转形式的支持,我们还是选择了 OSworkflow,而不是自己去实现一个基于状态机的工作流引擎。但是,我们还不能直接把 OSworkflow 放进项目里面,然后直接引用:一方面我们不想让开发团队都纠缠于底层 OSworkflow 的技术细节,另外一方面也是因为 OSworkflow 并不能完全满足客户的系统要求。我们需要扩展!

为什么去扩展?
依然是问答模式,OSworkflow 有哪些方面不能满足我们系统的要求?

1. 系统对各个流程的定义都能有版本跟踪

客户会对流程定义进行修改,但是流程修改的过程中,流程应该继续按照原来的定义进行流转。这样,对于同一份流程,系统会同时存在两个不同的定义,并且都能正常流转。OSworkflow 基于XML文件的流程配置方式在这种情况下就不能满足。

2. 系统对流程定义的修改能实现“hot deploy”

客户对流程定义进行修改后,系统能自动提供给用户新的流程定义,而不需要重启服务器。因为 OSworkflow 是在系统启动的时候,将所有的流程定义都载入内存,在系统运行期不会去检测流程定义的修改。这样,OSworkflow 也不能满足。

3. 相对于系统的要求,OSworkflow 提供的功能还是太复杂了

因为 OSworkflow 是提供一个通用的工作流引擎,所以考虑了很多复杂的情况。这些复杂性对我们系统来讲,都是额外的不能对客户产生价值的特性,而且那些复杂性的存在,也会让我们开发的时候必须考虑到那些方面,从而影响我们的模型设计和开发。

除了上文提到的几个原因,我们考虑的因素还有很多方面,最终我们决定是在OSworkflow基础上进行扩展,来满足我们的要求。

如何进行扩展?

如何进行扩展?我们的原则是什么?没有一个清晰的目标,很可能最终扩展的方向和最终实现就会跟我们想要的目标离得比较远了。

原则一. DDD,MDA

我们希望开发团队在设计开发的过程中只需要考虑系统领域的模型,而不会 involve 到底层的流程流转细节。

“find core domain models”,我们重新研究了系统的 proposal 和 lo-fi prototype,找出系统里面的领域对象,它们之间存在这样的一个关系:

图2 系统model
从图1 OSworkflow model 关系图和图2 系统 model 关系图可以看出,它们两者还是存在着一定的相似点和不同点的。

相同点:
1. 相应 model 完成的功能和职责是非常类似的
2. 不同 model 之见的关系也是非常类似的

不同点:
1. model 的属性
2. model 的一些行为

原则二. decoupling

我们希望项目实现不依赖于某个工作流产品,而是可以很方便地迁移到其他的工作流产品。第一想法就是——“laying, isolate by interface”。

其实工作流产品除了工作流引擎负责流程节点流转之外,都另外包括这三部分:流程定义、定义解析和状态持久化。而成熟的工作流产品,不管是JBPM,还是OSworkflow,都会提供接口进行隔离。在OSworkflow里面,接口WorkflowFactory会负责流程定义的解析和OSworkflow models的初始化,接口WorkflowStore则负责将Workflow和WorkflowEntry的状态持久化。这样,我们就可以定制实现接口,将domain models转换成 OSworkflow models,在流程流转需要持久化流程状态的时候,则会去持久化domain models。

经过这样几个方面的论证和分析,得到的最终方案如下:

  1. 使用domain model,而不是工作流model
  2. 扩展接口将 domain model 转换成工作流 model
  3. 扩展接口提供自定义的工作流状态持久化
  4. 不改动 osworkflow 内核

最终实现

图3 系统实现图

我们得到了什么

  1. 使用 domain model 进行开发
  2. 底层工作流引擎对开发团队透明
  3. 设计、优化工作在应用层次来进行

(本博未来会以同一个项目为例对敏捷项目各个阶段过程进行简单的介绍,敬请期待)

看上去很美 –OOafarian对play!框架的几点看法

 play! framework 是一个面向小型网站开发的 rails-like 的 Java 框架,不仅在目录结构上,在系统 skeleton 生成上,也把 rails 学了个七七八八。最近和同事也在做一个 rails style 的 Java Web 应用开发框架,参考了 rails 的很多 feature,但更多的是按照自己的开发理念和哲学思想“拿来” rails 里面有借鉴意义的思想。面对号称 rails-like 的 play! framework,这几天身在海滩上,于是花了一些时间好好研究了 play! 的源代码一番。“看上去很美”,是读完代码后脑海中第一下涌现出来的想法,我承认我是OOafarian。
 
 play! framework 5个“酷”的东西,看上去很美,但这些大都是建立在对 Java class 文件 hack 的基础上的。以 JPAModel 为例,任何它的子类 model 都是可以使用 rails style 的语法:User.findAll()。表面上看和 activerecord 的语法一样的,但实际上 play! 是通过在 JPAModel 里面定义这些静态方法,默认实现是抛出 RuntimeException,然后在 classloader 载入子类 class 文件的时候,通过增强每个 model class,使之具有这样类型安全的类方法。且不说这样实现是多么暴力和对开发人员不可理解,这与 rails 的实现是完全不一样的。ruby 因为是提供了 class 的继承体系,使得类方法也成为可以 override 的 class 类的实例方法,但是 Java 不提供 class 的继承和覆盖:这是 Java 的优势也是劣势。有人说这是语言的天性,但其实语言也是人们对世界进行抽象建模的一种折射,也反映的是人们的世界观和哲学思想。如果是我,如果依旧选择 Java 来做,我更倾向于保留 Java 的静态方法的优势和优雅,通过模式或者设计方法使之具有类似于动态语言的灵活性。这也是我和几位同事一致的看法。
 
 那么,从一位受 OO 和 pattern 熏陶很深的 Java 程序员看来,看上去很美的 play! framework 华丽的袍子下有多少虱子呢?待我细细数来:
 1. controller 里面充斥的 static void 方法,不 OO,不 testable,完全是过程化的代码。作者也是说了:
 
 The Java code from the Controller class isn’t really Object Oriented : it’s mainly some procedural code.
 
 当然,过程化还是面向对象,这个争论不是今天才有,不同的人在不同的上下文情景下都会选择最适合自己的开发方式。但是,作为受 OO 熏陶很深的我,这是不可接受的。
 
 2. view 的 render 实现是继承自 RuntimeException。记得很早之前,还有人争论 Exception 作为程序出口和条件判断分支的使用,但自《Effective Java》一书以及其他编码风格介绍的书籍出来之后,这样的争论早已不知何处。Exception 和 Error 会有自己适合的场所,也有它们自己的正确含义,被滥用可不是它们的错。我想,即使是 JVM 被优化得性能完全不用考虑了,这样的处理思路也是不可以被团队成员理解、认同和接受的吧?
 
 3. 暗地里修改代码,使用自己的classloader来大量增强或者修改class,甚至反编译 class。这样就导致很多增强的东西都是框架来强制性加入的,开发人员没有办法进行测试,也没有办法修改默认的实现。也许有人会觉得这样也挺好,觉得这样也够用,那也 OK。但是如果这些都建立在抛弃 Java 强大的静态类型安全,强大的 IDE 支持,强大的社区支持的基础之上,我是不能接受。
 
 当然,play! framework 也是体现了一些很新颖的创新。比如使用 Http Server 来取代 J2EE Server,甚至是 Application Server。又比如通过动态编译代码来实现 hot swap。还比如使用 java.lang.instruments API 来定位 Exception 的位置。这些创新在一定程度上减轻了 Java web 应用开发过程中的“重”,给开发人员带来“轻”的感觉。但是,如果这些需要付出 Java 社区积累到几天的成果,完全重头再来,我想我会宁愿选择 ruby on rails。
 
 当我们选择了 Java,我们看重的是强大的静态安全,看重的是强大的 IDE 支持和社区支持。虽然这些会给开发过程带来一定的“重”,但是米兰昆德拉说过,
 
    可是,沉重便真的悲惨,而轻松便真的辉煌吗?
    最沉重的负担压得我们崩塌了,沉没了,将我们钉在地上。可是在每一个时代的爱情诗篇里,女人总渴望压在男人的身躯之下。也许最沉重的负担同时也是一种生活最为充实的象征,负担越沉,我们的生活也就越贴近大地,越趋近真切和实在。
    相反,完全没有负担,人变得比大气还轻,会高高地飞起,离别大地亦即离别真实的生活。他将变得似真非真,运动自由而毫无意义。

 rails 之所以能轻,是因为它有 runit,有 rspec 来作为它的安全网,使之不会偏离方向。而 Java 也可以变得很轻,但这个轻却绝不是以抛弃 Java 的优势作为代价的。这也是我和几位同事在做 rails style 的 Java web 应用开发框架的过程中一直坚持的,希望到时能展现给大家一个高效率而有符合编程哲学的好框架。

DIP 沉思录

Dependency Injection 这个名词,是在 Martin Fowler 的《Inversion of Control Containers and the Dependency Injection pattern》文章之后才广为人知。在文章中,Martin 解释了当时初起流行的 IOC 概念:为了消除应用程序对插件实现的依赖,程序的主控权从应用程序移到了框架。为了让 IOC 概念不那么令人迷惑,Martin 把流行的几种 IOC Container 实现模式命名为 Dependency Injection Pattern(DIP,下文简称 DI 模式)。很明显,DI 的定义更准确形象,而且因为 Martin 在软件开发社区巨大的影响力,DI 模式以及 spring framework 作为其最成功的实现之一很快被软件开发社区接受并成为日常开发的必备利器。

“依赖注入”给我们代码的编写,特别是测试代码的编写带来了很多好处。借助于 DI 模式,我们可以将服务的依赖声明和具体实现相分离,通过配置不同的服务实例交给框架管理,来让应用程序获得良好的灵活性和柔性。

比如我们有这样的代码:

public void hello() {
Foo foo
= new Foo();
foo.sayHello();
//
}

这种情况下,hello() 方法就依赖于 Foo 类的 sayHello() 方法实现,会给测试带来了很多的不稳定性,比如耗时、服务异常之类。如果依照 DI 模式来重构,这段代码就会变成这样:

public void hello(Foo foo) {
foo.sayHello();
//
}

在测试时我们就可以使用行为确定符合预期的‘mock’的 Foo 类来代替实际的 Foo 类,从而将测试关注点聚集到 hello() 方法,也避免了 Foo 类 sayHello() 方法的具体实现对测试可能带来的影响。

以上就是 DIP 最常见也是最令人熟悉的一层涵义,从 IOC、spring framework 开始接受 DIP,自然而然会把 DIP 等同于 DI 模式,但其实它还有另一层涵义。在《Agile Software Developement:principles,Patterns,and Practices》的第11章 依赖倒置原则,给出了 DIP 的另一种解读:DIP —— 依赖倒置原则,这里的 DIP 就不再是 Dependency Injection Pattern(依赖注入模式),而是 Dependency Inversion Principle(依赖倒置原则,以下简称 DI 原则)。DI 原则主要包括下面两条启发式规则:
A. 高层模块不应依赖于低层模块。二者都应依赖于抽象
B. 抽象不应依赖于细节。细节应依赖于抽象

人们通常会用好莱坞法则来诠释启发规则 A:“Don’t call us, we’ll call you.”其中的 we/us,就是指高层模块,you 则是低层模块,高层模块来决定低层的模块,就像好莱坞制片人最终掌握着演员上镜与否的生杀大权。

我们来看看软件系统中常见的类关系,高层的 Policy Layer 使用了低层的 Mechanism Layer,而低层的 Mechanism Layer 又使用了更低层的 Utility Layer(见图1)

图 1

图1中的依赖关系是传递的:高层的 Policy Layer 对于其下一直到 Utility Layer 的改动都是敏感的,一旦我们修改了低层模块的实现,高层模块不得不也修改相应的实现来适应低层模块的修改。这种依赖关系,与软件复用的目标是相悖的,而且也不符合实际的业务变化。我们通常会需要切换低层的实现方式或者版本,而很少会去修改高层的业务。比如有一个证书申请发放系统,需要使用异步的消息机制来处理用户请求。客户可能会要求把底层的 MessageQueue 从 IIS 切换成 ActiveQ,但高层的证书申请发放的业务流程是稳定的,不会因低层基础服务的改变而作出修改。

所以,为了应付现实中的变化,也为了向高层屏蔽这些变化,我们通常会给低层模块抽象出接口,作为高层和低层之间的契约。这样,高层模块就应该只与低层模块的接口打交道,不再关心低层模块的实现细节。而低层模块的实现,我们可以通过配置文件由框架来切换,不影响到高层的行为。说到这里,大家应该可以看到 Spring 的影子了。此时类关系图就变成了下图(图2)

图 2

嗯,现在的类关系已经遵循接口依赖了,甚至加上了框架的 DI 模式,系统也具有了一定的灵活性和易改变性,看起来很像大多数的系统了。但是,这是不是就是符合 DI 原则呢?我们可以看到,这里的接口通常是由低层模块来定义和派生,也就是低层模块抽象出来提供给外界调用的服务接口。高层模块其实还是依赖于低层模块,只是这次高层模块产生依赖的是低层模块里面声明的接口。想起曾经在 javaeye 上看到一篇帖子,作者抱怨为什么 java 不提供 extracts 关键字,这样就可以由框架或者容器在具体类上抽出接口定义(与 implements 对应),省得手工创建这些接口。或许这是目前依赖注入框架带来的误区吧:使用人员不理解 DIP 的本意,单纯是为框架的约束而创建。此时声明的类关系显然还是没有完全体现 DI 原则的精髓。

在书中 Robert 进一步对 DI 原则给出了解释:“请注意这里的倒置不仅仅是依赖关系的倒置,也是接口所有权的倒置。我们通常会认为工具库应该拥有它们自己的接口,但是应用 DIP (DI 原则)时,我们发现往往是消费者拥有抽象接口,而它们的服务者则从这些接口派生。”基于这种思路,前面例子更符合面向对象思想的层次关系图如下(图3)
图 3

图3和图2的区别主要在接口的所有权:高层模块应该拥有接口所有权,低层模块派生自高层模块里定义的接口。这就意味着:
1. 高层模块引用的接口定义应该和高层模块的其他类放在一起
2. 高层模块的复用是把高层拥有的所有类和接口定义作为一个整体来复用的
3. 接口定义的改变只有根据高层模块的需要才进行的,而不是低层模块

OK,直到现在,我们才能说 DI 原则原来是这个意思,否则,体会不到就有可能误人误己。Robert 在本章的结论中说“事实上,这种依赖关系的倒置正是好的面向对象设计的标志所在,使用何种语言来编写程序是无关紧要的。如果程序的依赖关系是倒置的,它就是面向对象的设计。如果程序的依赖关系不是倒置的,它就是过程化的设计。”信哉此言!

那么,要按照 OOA & OOD 的设计思路来进行系统设计,DI 原则对我们有什么帮助呢?其实,DI 原则不仅仅是抽象的原则,而且是可以启发推导出出种种具体的实践。我们来看看对 OOA & OOD 的帮助。因为依赖关系是倒置的,就可以通过对高层策略的抽象和定义驱动出低层服务者的接口。以此类推,直到把最底层的模块设计出为止。那什么是高层策略呢?怎么找出潜在的抽象?书中同样给出了答案:“它是应用背后的抽象,是那些不随具体细节的改变而改变的真理。它是系统内部的系统——它是隐喻(metaphore)”更有详细的具体实践,敬请关注本博其他文章。

References:
Inversion of Control Containers and the Dependency Injection pattern,http://www.martinfowler.com/articles/injection.html,Martin Fowler
控制反转与依赖注入模式,http://gigix.blogdriver.com/diary/gigix/inc/DependencyInjection.pdf,熊节 译
《Agile Software Developement:principles,Patterns,and Practices》,Robert Fowler 著,邓辉 译,孟岩 审