6.3 正式启程
本章所需的背景知识你已经了解得差不多了,我们回到正题。本章将研究证券交易领域的各种现实用例,观察在Scala实现语言的作用下,如何将用例转化为生动的DSL。
我选择的用例跟之前讨论Ruby、Groovy实现时的用例差不多。这样你把前后的例子互相对照,就不难看出其中思路的变化。即使问题域相同,静态类型语言和动态语言的DSL设计思路也截然不同。首先我们了解一下Scala有哪些特性可以提升DSL语法的表现力。
Scala知识点
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的语法兼具表现力和简洁性
val a1 = ClientAccount(no = "acc-123", name = "John J.") ➊ 命名参数和默认参数
val a2 = ClientAccount(no = "acc-234", name = "Paul M.")
val accounts = List(a1, a2) ➋ 类型推断
val newAccounts =
ClientAccount(no = "acc-345", name = "Hugh P.") :: accounts ➌ :: 运算符是一个方法
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兼具面向对象和函数式功能双重优势的特点。
图6-3 Scala兼具面向对象编程和函数式编程功能,两者都可用于建设领域模型。运用Scala的面向对象特性,可以从类型和值的角度进行抽象,以子类型化的方式特化一个组件,然后通过mixin进行组合。而在函数式特性这边,Scala给你准备了高阶函数、闭包、组合子等工具。最后,可以用模块将两方面的成果合并起来,得到最终的抽象
要构建交易DSL的实现,首先从建立问题域的基本抽象开始。
1. 证券
代码清单6-2是对Instrument
的抽象,也就是对证券交易所中买卖的证券进行建模。代码中首先定义Instrument
的一般接口,然后针对Equity
类型以及几种FixedIncome
类型的证券进行特化。(如果需要了解各种证券类型的异同,请查阅4.3.2节的补充内容。)
代码清单6-2
Instrument
的Scala模型
package api
import java.util.Date
import Util._
sealed abstract class Currency(code: String) ➊ 几个单例对象
case object USD extends Currency("US Dollar")
case object JPY extends Currency("Japanese Yen")
case object HKD extends Currency("Hong Kong Dollar")
trait Instrument { ➋ Mixin式继承
val isin: String
}
case class Equity(isin: String, dateOfIssue: Date = TODAY)
extends Instrument ➋ Mixin式继承
trait FixedIncome extends Instrument { ➋ Mixin式继承
def dateOfIssue: Date
def dateOfMaturity: Date
def nominal: BigDecimal
}
case class CouponBond(
override val isin: String,
override val dateOfIssue: Date = TODAY,
override val dateOfMaturity: Date,
val nominal: BigDecimal,
val paymentSchedule: Map[String, BigDecimal])
extends FixedIncome
case class DiscountBond(
override val isin: String,
override val dateOfIssue: Date = TODAY,
override val dateOfMaturity: Date,
val nominal: BigDecimal,
val percent: BigDecimal)
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实现
package api
abstract class AccountType(name: String)
case object CLIENT extends AccountType("Client")
case object BROKER extends AccountType("Broker")
import Util._
import java.util.Date
abstract class Account(no: String, name: String, openDate: Date) {
val accountType: AccountType
private var closeDate: Date = _
var creditLimit: BigDecimal = 100000 设置默认信用额度
def close(date: Date) = {
closeDate = date
}
}
case class ClientAccount(no: String, name: String,
openDate: Date = TODAY)
extends Account(no, name, openDate) {
val accountType = CLIENT
}
case class BrokerAccount(no: String, name: String,
openDate: Date = TODAY)
extends Account(no, name, openDate) {
val accountType = BROKER
}
除了Account
和Instrument
模型,我们还需要一个代表证券交易本身的基本抽象。
代码清单6-4
Trade
模型的Scala实现
package api
import java.util.Date
trait Trade {
def tradingAccount: Account
def instrument: Instrument
def currency: Currency
def tradeDate: Date
def unitPrice: BigDecimal
def quantity: BigDecimal
def market: Market
def principal = unitPrice * quantity
var cashValue: BigDecimal = _
var taxes: Map[TaxFee, BigDecimal] = _
}
trait FixedIncomeTrade extends Trade {
override def instrument: FixedIncome ➊ 覆盖方法,特化返回类型
var accruedInterest: BigDecimal = _
}
trait EquityTrade extends Trade {
override def instrument: Equity ➊ 覆盖方法,特化返回类型
}
按照交易证券所属的类,我们定义了两种类型的交易。稍后你会了解,这两种类型的交易具有不同的特征,尤其是现金价值的计算方法很不一样。(关于交易的现金价值请参阅4.2.2节的补充内容。)代码清单6-4还有一点值得注意,我们覆盖了instrument
方法➊,让它的返回类型正确地反映每一类交易所针对的证券类型。
我们仅仅为编写交易创造DSL而设置相关上下文就已经写了这么多代码,下一节该正式动笔了。顺便还要谈谈构建中用得上的Scala特性。