A.3 精炼,只保留自身需要的

在第A.1节的讨论中,我们说过,抽象应该只向其客户公开核心的、必不可少的部分,这样从外部看起来,抽象是简洁的;而抽象内部的设计是否简洁,也同样重要。所谓精炼,即指从事物中萃取其本质的过程。对抽象设计来说,精炼是指去除实现中的非本质细节,使抽象的实现保持纯粹的过程。

A.3.1 什么是非本质的

你肯定会问,如何才能知道哪部分实现是非本质的?我引用专家的话来回答:带着深刻的认识和清醒的头脑去研究你的抽象,有过这样的经验,自然能够辨别出来。还可以下一个非正式的定义:若抽象中的一处细节不能映射到某个核心事项,那么该处细节就是非本质的。

假设你打算找一份领域建模师的工作,于是打开文字处理软件起草求职申请。你打字的同时,软件通过内置的拼写检查器标出不正确的字词。那么,你什么时候关心过软件自带拼写检查器的确切版本?又有哪次拼写检查器没有随软件启动,而需要你专门开启?理所当然地,你会假定软件打开的时候,拼写检查功能已经准备就绪,将会正确无误地运行。如果每打一个字都要特地检查、开启一遍,反而说明流程中含有非本质的细节。

如何提炼抽象,去除非本质细节?对于这方面基本概念的解释,会再次用到基于类的面向对象编程中的一种常见模式。例子照旧来自金融中介系统领域,如果对此领域不熟悉,可以翻阅第1.2和1.3节的插叙,那里介绍了一些相关的概念。

A.3.2 非本质复杂性

交给你一个任务:设计一个TradeProcessor抽象,当它收到提交的一组交易时,将计算出各种交易细节,包括交易净值、应付的各种税费和佣金,还有与交易市场相关的其他信息。你设计出来的抽象大概如同代码清单A-1的样子。

代码清单A-1 TradeProcessor处理各种交易细节

  1. class TradeProcessor {
  2. private SettlementDateCalculator calculator;
  3. public TradeProcessor() {
  4. try {
  5. calculator = new SettlementDateCalculatorImpl(..);
  6. } catch (InitializationException ex) { //.. }
  7. }
  8. public void process(List<Trade> trades) {
  9. for(Trade trade: trades) {
  10. calculator.settleOn(trade.getTradeDate());
  11. }
  12. // 其余处理过程
  13. }
  14. }

TradeProcessor需要按照“交易充实”过程的规定计算结算日,该日期的计算牵涉成交前后的各种因素,由SettlementDateCalculator负责提供计算服务。假设SettlementDateCalculator是一个独立的接口,SettlementDateCalculatorImpl是其实现,负责根据成交日期及其他上下文信息确定结算日。TradeProcessor的构造器创建一个SettlementDateCalculatorImpl的实例并保存在上下文中,供process方法后续使用。也就是说TradeProcessor实例初始化了一个服务,就要对其全生命周期负责。SettlementDateCalculatorImpl的构造器可能很复杂,需要其他服务的协助才能成功初始化。

万一哪个服务初始化失败了呢?如果遇到这样的情况,TradeProcessor类要负责在其构造器中处理因此发生的异常连锁反应,并安排相应的恢复措施。那么,TradeProcessor作为一个领域抽象,应不应该由它来考虑这些事情呢?

TradeProcessor是一个领域对象。那么在设计它的时候,就应该只把它当做一个领域对象来规划,考虑这个领域对象如何配合其他领域对象和领域服务,完成它向客户承诺的职责。至于如何实例化,如何管理各种服务的生命周期,这些都不是领域抽象的核心职责。从上面的例子可以看出,领域对象的设计很容易把一些非本质的复杂性也参杂在内,而其实把这些方面放到更低的架构层次去处理更加合适。Fred Brooks把这种情况称为偶然复杂性(accidental complexity,参见第A.6节文献[1]),也有人称为次要复杂性(incidental complexity)。

enter image description here只有当抽象中的非本质复杂性被限制到最少时,才能保证抽象处于一个恰当的层次。程序设计者应该注意把实例化和相关服务的生命周期管理委托给依赖注入(DI)容器等底层框架。

对设计过程的每一步都应该进行回顾,检查一下抽象是否足够精炼,是否有低层的细节被泄露到高层的抽象。如果发现像TradeProcessor类一样的情况,就知道应该重新调整上下层架构的职责分配,消除非本质复杂性。

A.3.3 撇除杂质

消灭非本质复杂性的药方,还是那个解决了很多计算机科学问题的老法子——在实现语言和领域抽像之间引入一层新的间接层。新的间接层将领域抽象隔离起来,保护它免受非本质复杂性的侵染。

在思考撇除杂质的方法之前,我们先换个角度,看看哪些成分属于杂质。我们从抽象的代码中截取另一段来进行讨论,应该撇除的信息已经做了标注。

代码清单A-2 标注了非本质细节的TradeProcessor 版本

  1. class TradeProcessor {
  2. private final SettlementDateCalculator calculator;
  3. public TradeProcessor() {
  4. try {
  5. calculator = new SettlementDateCalculatorImpl(..); →❶管理生命周期的代码
  6. } catch (InitializationException ex) { //.. } →❷服务失败时的异常处理
  7. }
  8. }

TradeProcessor在构造器中实例化SettlementDateCalculator的一个具体实现❶,担负了管理SettlementDateCalculator服务生命周期的职责。因此产生以下后果。

  • 从此TradeProcessor对该服务的某个特定具体实现产生依赖。

    TradeProcessor编写单元测试时,必须保证该服务实例已经就位,还要保证所有被依赖的服务全部就位。这违反了抽象应该可进行独立单元测试的原则,而且一旦脱离这个具体的实现环境,抽象被重用的可能性大大降低。

  • TradeProcessor的构造器被SettlementDateCalculatorImpl的初始化逻辑引发的错误处理代码污染❷。

代码中充斥着噪杂的细节,这些细节不应该成为TradeProcessor的首要关注方面。

对于哪些成分属于抽象中的非本质细节,你有所了解了吗?很好!让我们来消灭它们吧。

A.3.4 用DI隐藏实现细节

我们要做的就是从TradeProcessor的代码中去除涉及SettlementDateCalculator生命周期管理的部分。正确的方式是,当TradeProcessor需要SettlementDateCalculator时,我们从外部给它提供一个实例。TradeProcessor不需要关心收到的实例属于哪个确切的具体实现,因为相关服务的实例化、管理和终结等具体事务将与TradeProcessor隔离。依赖注入(dependency injection,DI)会替我们完成隔离工作。

定义 DI框架是一种外部容器,它通过说明式的配置代码,创建、装配、连接各种依赖项,把它们组织成一个对象图。关于各种DI技术的详细说明,请参阅第A.6节文献[2]。

我们使用Google提供的Guice依赖注入框架(http://code.google.com/p/google-guice/),把依赖绑定到SettlementDateCalculator的具体实现。下面的代码是抽象精炼之后的样子。

代码清单A-3 精炼后的TradeProcessor

  1. class TradeProcessor {
  2. private final SettlementDateCalculator calculator;
  3. @Inject 指示注入位置的标注
  4. public TradeProcessor(SettlementDateCalculator calculator) {
  5. this.calculator = calculator; 干净的构造器
  6. }
  7. //.. 同代码清单A-2
  8. }

现在,TradeProcessor摆脱了非本质的细节,实例化逻辑也被移交到外部框架。目前来说,你只需要了解如何绑定TradeProcessor类和SettlementDateCalculator的具体实现即可。我们需要在Guice里面配置一个外部Module,Guice会在应用程序启动时完成绑定。Guice的工作原理在此不作介绍,重点是通过引入一个外部框架,得以去除原先抽象中的所有非本质细节。

精炼抽象、撇除非本质复杂性的手段有很多,DI仅仅是其中一种。很多语言实现直接提供了强有力的语言特性,不必借助外部框架来达到这样的目标。第6章讲述Scala语言强大且可扩展的静态类型系统时,就有这方面的例子。函数式编程里的高阶函数和闭包可以把功能外部化,也就是把非本质细节放到抽象外部去处理。第5章详细讨论了如何使用函数式编程特性来设计领域模型,也将深入介绍了这方面的例子。