6.6 把组件装配起来
带着为税费计算业务规则建立DSL的经验,我们来准备下一道DSL大菜,这道菜的材料还欠几味抽象。
Scala知识点
Scala的模块,即
object
关键字。允许通过组合抽象的组件来定义具体的实例。各种组合子,如
map
、foldLeft
、foldRight
。
本节将介绍怎样运用Scala的mixin式继承来组合不同的trait。你还会看到Scala语言支持的另一种抽象形式——基于类型的抽象。可供选择的抽象组合手段越多,领域模型的可塑性就越强,也越容易衍生出合适的DSL语法。
6.6.1 用trait和类型组合出更多的抽象
领域模型里面有一部分抽象扮演对外的窗口,直接面向最终用户,领域服务即为其中之一。领域服务使用各种实体和值对象(详见6.10节文献[6]),向用户履行契约。在代码清单6-10中,TradingService
是一个典型的领域服务,但比真实的使用案例简化很多。
代码清单6-10 服务基类
TradingService
的Scala实现
package api
trait TradingService
extends TaxFeeCalculationComponent
with TaxFeeRulesComponent { ➊ 利用traits完成的mixin式继承
type T <: Trade ➋ 抽象类型
def taxes(trade: T) =
taxFeeCalculator.calculateTaxFee(trade)
def totalTaxFee(trade: T): BigDecimal = { ➌ 基于组合子的编程方式
taxes(trade).foldLeft(BigDecimal(0))(_ + _._2)
}
def cashValue(trade: T): BigDecimal ➍ 抽象方法
}
图6-6简要讲解了服务契约的履行过程,同时指出其中用到的一些Scala特性。
表6-6 剖析代码清单6-10中的TradingService
领域服务
特性 | 说明 |
---|---|
mixin与现有抽象组合在一起的强大能力 | TradingService 被混入了两个组件,TaxFeeCalculationComponent 和TaxFeeRulesComponent ➊
注意:
在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 针对股票交易的交易服务具体实现
package api
object EquityTradingService
extends TradingService {
type T = EquityTrade ➊ 提供具体的类型
val taxFeeCalculator = new TaxFeeCalculatorImpl ➋ 提供具体的val
val taxFeeRules = new TaxFeeRulesImpl ➋ 提供具体的val
override def cashValue(trade: T): BigDecimal = { ➌ 提供具体的方法
trade.principal + totalTaxFee(trade)
}
}
看上去挺简单的,对吧?写法有以下几个要点:
用一个具体类型
EquityTrade
➊代替基类中定义的抽象交易类型;先前混入到基类的trait还留下几个抽象的val,我们要一一提供具体的实现➋;
针对股票交易的
cashValue
,给出具体的计算方法➌。
参照EquityTradingService
的实现方式,我们继续实现固定收益型交易FixedIncomeTrade
对应的具体交易服务FixedIncomeTradingService
,如代码清单6-12所示。
代码清单6-12 Scala中针对固定收益型交易的交易服务
package api
object FixedIncomeTradingService
extends TradingService with AccruedInterestCalculationComponent { ➊ 添加计算应计利息的mixin
type T = FixedIncomeTrade
val taxFeeCalculator = new TaxFeeCalculatorImpl
val accruedInterestCalculator = new AccruedInterestCalculatorImpl 应计利息计算器的具体实例
val taxFeeRules = new TaxFeeRulesImpl
def accruedInterest(trade: T): BigDecimal = {
accruedInterestCalculator.calculateAccruedInterest(trade)
}
override def cashValue(trade: T): BigDecimal = {
trade.principal +
accruedInterest(trade) + totalTaxFee(trade)
}
}
注意,我们在核心抽象上额外混入了AccruedInterestCalculationComponent
组件➊,它的功能是计算“应计利息”。固定收益型证券一般都含有应计利息,而且固定收益型交易的现金价值应该将此利息计算在内。这条业务规则不难从FixedIncomeTradingService
的定义中看出来。
本节先定义了领域服务抽象,然后将前面几节构建的组件装配上去,构造出可以在DSL中直接使用的具体Scala模块。
这个练习展示了Scala的真正威力,它可以推迟到最后时刻才进行具体的实现。Scala的这种能力源自抽象val、抽象类型、自类型标注这三大支柱的支撑。除此之外,mixin式继承灵活的抽象组合能力也起了很大作用。Scala给了你丰富的手段去设计可扩展的各式组件。
我们在基础领域模型组件的基础上构建了一套DSL。就Scala而言,我把DSL层看做根据用户需求逐渐演化的一组抽象。按照这样的思路,在对交易系统中的不同用例进行建模之后,你将得到一个多层次的组合抽象模型。当市场规则有变,需要在现有抽象上加入新规则时,也就是当现有DSL需要与新DSL携手合作时,应该怎样实施?下一节说说怎么用Scala的类型系统来做这件事。