6.6 把组件装配起来

带着为税费计算业务规则建立DSL的经验,我们来准备下一道DSL大菜,这道菜的材料还欠几味抽象。

6.6	把组件装配起来 - 图1Scala知识点

  • Scala的模块,即object关键字。允许通过组合抽象的组件来定义具体的实例。

  • 各种组合子,如mapfoldLeftfoldRight

本节将介绍怎样运用Scala的mixin式继承来组合不同的trait。你还会看到Scala语言支持的另一种抽象形式——基于类型的抽象。可供选择的抽象组合手段越多,领域模型的可塑性就越强,也越容易衍生出合适的DSL语法。

6.6.1 用trait和类型组合出更多的抽象

领域模型里面有一部分抽象扮演对外的窗口,直接面向最终用户,领域服务即为其中之一。领域服务使用各种实体和值对象(详见6.10节文献[6]),向用户履行契约。在代码清单6-10中,TradingService是一个典型的领域服务,但比真实的使用案例简化很多。

代码清单6-10 服务基类TradingService的Scala实现

  1. package api
  2. trait TradingService
  3. extends TaxFeeCalculationComponent
  4. with TaxFeeRulesComponent { 利用traits完成的mixin式继承
  5. type T <: Trade 抽象类型
  6. def taxes(trade: T) =
  7. taxFeeCalculator.calculateTaxFee(trade)
  8. def totalTaxFee(trade: T): BigDecimal = { 基于组合子的编程方式
  9. taxes(trade).foldLeft(BigDecimal(0))(_ + _._2)
  10. }
  11. def cashValue(trade: T): BigDecimal 抽象方法
  12. }

图6-6简要讲解了服务契约的履行过程,同时指出其中用到的一些Scala特性。

表6-6 剖析代码清单6-10中的TradingService领域服务

特性 说明
mixin与现有抽象组合在一起的强大能力 TradingService被混入了两个组件,TaxFeeCalculationComponentTaxFeeRulesComponent注意: 在mixin方式下,我们不但继承了接口,还继承了可选的实现。mixin才是正确的多重继承机制
针对交易类型的抽象 TradingService对交易类型做了抽象➋。这样的安排很好理解,因为不同类型的交易需要区别对待,由各自专门的交易服务去处理。虽然服务基类TradingService不理会具体的交易类型,但交易必须满足从属于Trade基类这个基本条件 什么时候具体化类型T 到了具体化TradingService时,我们会为抽象交易类型T提供一个具体实现
税费计算的核心逻辑位于totalTaxFee方法 服务中定义了具体方法totalTaxFee➌,用于合计组件中算出的税费项目,计算通过foldLeft组合子完成。Scala组合子foldLeft的原理以及占位符“_”的用法详见附录D 提示: 应该优先使用组合子,其次才考虑递归或迭代
通过抽象方法将实现工作推给子类 cashValue是抽象方法➍,因为具体的逻辑与服务要处理的交易类型有关,所以把它留给子类型去实现

到目前为止我们还没有具体化任何抽象,几个trait都还带着抽象类型,下一节将给出定义。Scala语言提供了异常丰富的抽象设计手段供你选择。设计时,应该针对手头的问题,挑选最合适的工具,也别忘了参照附录A讨论的设计原则。

6.6.2 使领域组件具体化

EquityTradingService为股票交易提供交易服务。它是一个具体的组件,只需针对它生成的服务实例化一次。代码清单6-11用Scala的单例对象表示法(具体参见6.10节文献[2])来建模EquityTradingService

代码清单6-11 针对股票交易的交易服务具体实现

  1. package api
  2. object EquityTradingService
  3. extends TradingService {
  4. type T = EquityTrade 提供具体的类型
  5. val taxFeeCalculator = new TaxFeeCalculatorImpl 提供具体的val
  6. val taxFeeRules = new TaxFeeRulesImpl 提供具体的val
  7. override def cashValue(trade: T): BigDecimal = { 提供具体的方法
  8. trade.principal + totalTaxFee(trade)
  9. }
  10. }

看上去挺简单的,对吧?写法有以下几个要点:

  • 用一个具体类型EquityTrade➊代替基类中定义的抽象交易类型;

  • 先前混入到基类的trait还留下几个抽象的val,我们要一一提供具体的实现➋;

  • 针对股票交易的cashValue,给出具体的计算方法➌。

参照EquityTradingService的实现方式,我们继续实现固定收益型交易FixedIncomeTrade对应的具体交易服务FixedIncomeTradingService,如代码清单6-12所示。

代码清单6-12 Scala中针对固定收益型交易的交易服务

  1. package api
  2. object FixedIncomeTradingService
  3. extends TradingService with AccruedInterestCalculationComponent { 添加计算应计利息的mixin
  4. type T = FixedIncomeTrade
  5. val taxFeeCalculator = new TaxFeeCalculatorImpl
  6. val accruedInterestCalculator = new AccruedInterestCalculatorImpl 应计利息计算器的具体实例
  7. val taxFeeRules = new TaxFeeRulesImpl
  8. def accruedInterest(trade: T): BigDecimal = {
  9. accruedInterestCalculator.calculateAccruedInterest(trade)
  10. }
  11. override def cashValue(trade: T): BigDecimal = {
  12. trade.principal +
  13. accruedInterest(trade) + totalTaxFee(trade)
  14. }
  15. }

注意,我们在核心抽象上额外混入了AccruedInterestCalculationComponent组件➊,它的功能是计算“应计利息”。固定收益型证券一般都含有应计利息,而且固定收益型交易的现金价值应该将此利息计算在内。这条业务规则不难从FixedIncomeTradingService的定义中看出来。

本节先定义了领域服务抽象,然后将前面几节构建的组件装配上去,构造出可以在DSL中直接使用的具体Scala模块。

enter image description here这个练习展示了Scala的真正威力,它可以推迟到最后时刻才进行具体的实现。Scala的这种能力源自抽象val抽象类型自类型标注这三大支柱的支撑。除此之外,mixin式继承灵活的抽象组合能力也起了很大作用。Scala给了你丰富的手段去设计可扩展的各式组件。

我们在基础领域模型组件的基础上构建了一套DSL。就Scala而言,我把DSL层看做根据用户需求逐渐演化的一组抽象。按照这样的思路,在对交易系统中的不同用例进行建模之后,你将得到一个多层次的组合抽象模型。当市场规则有变,需要在现有抽象上加入新规则时,也就是当现有DSL需要与新DSL携手合作时,应该怎样实施?下一节说说怎么用Scala的类型系统来做这件事。