6.3 正式启程

本章所需的背景知识你已经了解得差不多了,我们回到正题。本章将研究证券交易领域的各种现实用例,观察在Scala实现语言的作用下,如何将用例转化为生动的DSL。

我选择的用例跟之前讨论Ruby、Groovy实现时的用例差不多。这样你把前后的例子互相对照,就不难看出其中思路的变化。即使问题域相同,静态类型语言和动态语言的DSL设计思路也截然不同。首先我们了解一下Scala有哪些特性可以提升DSL语法的表现力。

6.3 正式启程 - 图1Scala知识点

  • Scala语言的面向对象特性。Scala的类和继承结构有多种设计方式。

  • Scala语言拥有类型推断特性;它的运算符等同于方法;语法灵活,可省略圆括号和分号。

  • 不可变变量(immutable variable)有利于设计函数式的抽象。

  • Scala语言的 case类和case对象,其特点适用于设计不可变的值对象。

  • Scala的trait特性,可用于设计mixin和多继承。

6.3.1 语法层面的表现力

一门语言的语法是富于表现力还是繁冗拖沓,只有一线之隔。非程序员用户觉得表现充分的语法,程序员可能就觉得烦琐到了极点。5.2.3节为交易DSL设计Ruby解释器时就说过这个问题。还记得2.1.2节老鲍的抱怨吗?Java给指令处理DSL强加了一些不必要的语法复杂性,老鲍很有意见。Scala虽然也和Java语言一样是静态类型的,但它对外呈现了较为精简的表面语法,可以减少对DSL的干扰。代码清单6-1展示了一段常见的Scala代码,它的功能是将ClientAccount对象放入已存在的一个账户列表。

代码清单6-1 Scala的语法兼具表现力和简洁性

  1. val a1 = ClientAccount(no = "acc-123", name = "John J.") 命名参数和默认参数
  2. val a2 = ClientAccount(no = "acc-234", name = "Paul M.")
  3. val accounts = List(a1, a2) 类型推断
  4. val newAccounts =
  5. ClientAccount(no = "acc-345", name = "Hugh P.") :: accounts :: 运算符是一个方法
  6. newAccounts drop 1 可省略的圆括号

即使你不熟悉Scala语言,也能毫无困难地看懂这段代码。表6-2列出的几项特性正是代码简明的关键因素。

表6-2 使Scala语法简洁的各项特性,以代码清单6-1为参照

Scala的简洁性来源 对DSL设计的影响
自动推断句末分号 Scala与Java不同,不要求在语句间加上分号作为分隔符,直接降低了对DSL语法的干扰
命名参数和默认参数 ClientAccount类实例化时➊用了命名参数。该类被声明为case类(见代码清单6-3),所以其对象的构造语法较为简便。还有部分参数因为已经在类定义中设置了默认值,所以实例化时可以不用写出来。命名参数和默认参数对于提高DSL脚本的可读性有很大帮助
类型推断 用多个账户对象组成列表时,不需要指定结果列表的类型➋,编译器会帮你推断
运算符等同于方法 我们通过::运算符向accounts列表增加一个ClientAccount对象➌。实际上等同于在List实例上调用方法,即accounts.::(ClientAccount(no = "acc-345", name = "Hugh P."))。写成运算符的形式,同时省略点号(.),这两项措施大大提升了代码片段的可读性
可省略圆括号 我们调用List类的drop方法从列表中删去第一个账户➍。此处代码省略了圆括号,更贴近一般的阅读习惯

本小节讨论的只是一些纯语法层面的表面因素。还有其他特性同样对Scala语法的可读性起了正面的作用,包括它强大的集合字面量语法、允许用闭包作为控制抽象等特性,也包括隐含参数等高级特性。本章将逐一介绍这些特性,讨论它们各自在内部DSL设计中的作用。

为了给后面的DSL打好基础,现在我们需要着手准备一些基本的领域抽象。我们的抽象仍然出自前面几章打过不少交道的证券交易领域。一方面不至于浪费前面学到的领域概念,另一方面也便于在对比中学习各种语言的不同实现惯例。

6.3.2 建立领域抽象

设计Scala DSL时,通常需要有一个对象模型作为基本抽象层。然后运用子类型化手段,实现各种模型组件的特化,再将它们与解答域中符合条件的mixin组合起来,构成更大型的抽象。模型中的操作可通过函数抽象来表示,然后用组合子来组织它们。图6-3说明了Scala抽象实现扩展性的方式,它利用了Scala兼具面向对象和函数式功能双重优势的特点。

enter image description here

图6-3 Scala兼具面向对象编程和函数式编程功能,两者都可用于建设领域模型。运用Scala的面向对象特性,可以从类型和值的角度进行抽象,以子类型化的方式特化一个组件,然后通过mixin进行组合。而在函数式特性这边,Scala给你准备了高阶函数、闭包、组合子等工具。最后,可以用模块将两方面的成果合并起来,得到最终的抽象

要构建交易DSL的实现,首先从建立问题域的基本抽象开始。

1. 证券

代码清单6-2是对Instrument的抽象,也就是对证券交易所中买卖的证券进行建模。代码中首先定义Instrument的一般接口,然后针对Equity类型以及几种FixedIncome类型的证券进行特化。(如果需要了解各种证券类型的异同,请查阅4.3.2节的补充内容。)

代码清单6-2 Instrument的Scala模型

  1. package api
  2. import java.util.Date
  3. import Util._
  4. sealed abstract class Currency(code: String) 几个单例对象
  5. case object USD extends Currency("US Dollar")
  6. case object JPY extends Currency("Japanese Yen")
  7. case object HKD extends Currency("Hong Kong Dollar")
  8. trait Instrument { Mixin式继承
  9. val isin: String
  10. }
  11. case class Equity(isin: String, dateOfIssue: Date = TODAY)
  12. extends Instrument Mixin式继承
  13. trait FixedIncome extends Instrument { Mixin式继承
  14. def dateOfIssue: Date
  15. def dateOfMaturity: Date
  16. def nominal: BigDecimal
  17. }
  18. case class CouponBond(
  19. override val isin: String,
  20. override val dateOfIssue: Date = TODAY,
  21. override val dateOfMaturity: Date,
  22. val nominal: BigDecimal,
  23. val paymentSchedule: Map[String, BigDecimal])
  24. extends FixedIncome
  25. case class DiscountBond(
  26. override val isin: String,
  27. override val dateOfIssue: Date = TODAY,
  28. override val dateOfMaturity: Date,
  29. val nominal: BigDecimal,
  30. val percent: BigDecimal)
  31. extends FixedIncome

领域语汇在上面的实现中表达得很清晰,而且领域模型基本没有受到偶发复杂性的干扰。(关于偶发复杂性的详细讨论请参阅附录A。)这是本章建立的第一个领域模型,我们不妨在它身上多下一些功夫,看看哪些Scala特性对模型的表现力和简洁性有所贡献。

  • 单例(singleton)对象,因为它只实例化一次的特点,此处被用于实现Currency类➊的几个特化实体。单例对象是Scala语言实现Singleton模式(参见6.10节文献[3])的方式,弥补了Java语言中静态成员的所有不足。

  • 在trait的组织下,通过继承➋来实现一种可扩展的对象层次关系。

  • case类具有简化的构造函数调用形式。

我们还要再构建几个抽象才能开始编写DSL脚本。

2. 账户和交易

代码清单6-3是Account模型的Scala实现。客户与中介在Account这个领域实体上交易各种证券。

代码清单6-3 Account模型的Scala实现

  1. package api
  2. abstract class AccountType(name: String)
  3. case object CLIENT extends AccountType("Client")
  4. case object BROKER extends AccountType("Broker")
  5. import Util._
  6. import java.util.Date
  7. abstract class Account(no: String, name: String, openDate: Date) {
  8. val accountType: AccountType
  9. private var closeDate: Date = _
  10. var creditLimit: BigDecimal = 100000 设置默认信用额度
  11. def close(date: Date) = {
  12. closeDate = date
  13. }
  14. }
  15. case class ClientAccount(no: String, name: String,
  16. openDate: Date = TODAY)
  17. extends Account(no, name, openDate) {
  18. val accountType = CLIENT
  19. }
  20. case class BrokerAccount(no: String, name: String,
  21. openDate: Date = TODAY)
  22. extends Account(no, name, openDate) {
  23. val accountType = BROKER
  24. }

除了AccountInstrument模型,我们还需要一个代表证券交易本身的基本抽象。

代码清单6-4 Trade模型的Scala实现

  1. package api
  2. import java.util.Date
  3. trait Trade {
  4. def tradingAccount: Account
  5. def instrument: Instrument
  6. def currency: Currency
  7. def tradeDate: Date
  8. def unitPrice: BigDecimal
  9. def quantity: BigDecimal
  10. def market: Market
  11. def principal = unitPrice * quantity
  12. var cashValue: BigDecimal = _
  13. var taxes: Map[TaxFee, BigDecimal] = _
  14. }
  15. trait FixedIncomeTrade extends Trade {
  16. override def instrument: FixedIncome 覆盖方法,特化返回类型
  17. var accruedInterest: BigDecimal = _
  18. }
  19. trait EquityTrade extends Trade {
  20. override def instrument: Equity 覆盖方法,特化返回类型
  21. }

按照交易证券所属的类,我们定义了两种类型的交易。稍后你会了解,这两种类型的交易具有不同的特征,尤其是现金价值的计算方法很不一样。(关于交易的现金价值请参阅4.2.2节的补充内容。)代码清单6-4还有一点值得注意,我们覆盖了instrument方法➊,让它的返回类型正确地反映每一类交易所针对的证券类型。

我们仅仅为编写交易创造DSL而设置相关上下文就已经写了这么多代码,下一节该正式动笔了。顺便还要谈谈构建中用得上的Scala特性。