1.3 初窥DSL
蹦蹦高证券公司的IT主管乔搞不懂交易员老鲍在做什么事情,不自觉瞥了一眼他的电脑屏幕。令他感到惊讶的是,乔发现老鲍正忙着往编程环境——乔觉得只有其手下的开发团队才能用的编程环境——里敲一些命令和语句。下面是他们之间的对话。
乔:嘿,老鲍,你还会编程?
老鲍:嗯,会点儿,可以在我们的新系统TrampolineEasyTrade中编点儿。
乔:可你不是交易员吗?
老鲍:交易员怎么了?交易员就用这个软件做交易。
乔:软件是提供给你们使用的,没打算让你们在里头编程。而且,这产品还没开发完呢。
老鲍:可既然是我将来要用的软件,现在我给它编一些测试不是挺好吗?这样一来,我的意见可以尽早反馈给开发团队;近距离参与,自我感觉贡献更大;我会对开发中的产品更有信心。而且,我还可以验证我的用例能不能通过。
乔:但这是开发团队的职责!我每天都和他们沟通。我们有工具检查代码覆盖率、测试覆盖率以及各种指标,肯定能保证交付最好的软件。
老鲍:就对金融中介系统这个领域的了解而言,你觉得谁更懂?我,还是你的那套工具?
最后,乔不得不认同老鲍身为金融中介系统领域的专家,更有能力检查新交易平台是否正确、充分地满足了功能规格书的要求。只不过乔还是不懂,为什么老鲍不是程序员却也会用测试框架写测试。
读者想必也有同样的疑惑。那就来看看下面的代码清单,这正是“刚刚”老鲍在屏幕上敲出的内容。
代码清单1-1 用DSL编写的交易单处理过程
place orders (
new Order to buy(100 sharesOf "IBM")
limitPrice 300
allOrNone
using premiumPricing,
new Order to buy(200 sharesOf "CISCO")
limitOnClosePrice 300
using premiumPricing,
new Order to buy(200 sharesOf "GOOGLE")
limitOnOpenPrice 300
using defaultPricing,
new Order to sell(200 bondsOf "SUN")
limitPrice 300
allOrNone
using {
(qty, unit) => qty * unit - 500
}
)
好像真是代码呢。没错,但同时代码里的一些用语就跟老鲍平常坐上交易台时说的那种语言一样。老鲍正在编写一段创建新交易单的脚本,里面按照不同定价策略生成了各种交易单的样本。除了预定义的策略,他还可以在下单的时候自定义一种定价策略。
老鲍编程用的是什么语言?只要能完成工作,他一点儿都不在意。对于他来说,这种编程语言跟他在交易台上用的没什么两样。不过对于我们来说,老鲍所做的事情与程序员们日常编写代码的工作有何不同,这值得细辨一番。
老鲍的编程语言用语契合他所属的领域。他可以把平日在交易台前为客户下单所用的术语原封不动地写进测试脚本。
他所用的语言,从刚才所见的那一段来看,并不适用于金融中介业务以外的领域。
这种语言的表现力很强,老鲍只需要照着平常给客户创建新定单的步骤按部就班,就能把要做的事情清晰地表达出来。
这种语言语法简明。高级编程语言中常见的复杂语法细节奇迹般地不见了。
老鲍所用的正是一种为金融中介系统量身订做的领域专用语言。此刻,背后用什么语言来实现DSL显得无关紧要。我们很难从代码清单1-1中看出来背后用的是哪种语言,这正好说明设计者成功地为该领域创造了一种富于表现力的语言。
1.3.1 何为DSL
DSL是一种针对特定问题的编程语言,我们平常所用的编程语言用途更为宽泛。DSL含有建模所需的语法和语义,在与问题域相同的抽象层次对概念建模。例如,你要点一份肉桂拿铁咖啡,就对店员使用她所掌握的领域语言。
定义 抽象是人类大脑的一种认知过程,它使我们集中注意力于认知对象的核心层面,忽略不必要的细节。1.7节会进一步讨论抽象与DSL设计的关系,附录A则完全是关于抽象的内容。
用DSL写出来的程序,任何一方面的品质都不应该低于用其他计算机语言编写的程序。DSL还应该赋予你设计领域中抽象概念的能力。在问题域可以用小的实体搭建出大的实体,那么在解答域,设计得当的DSL应该给予你同样灵活的组合能力,让你能够就像编排问题域的各种机能一样编排起各种DSL抽象。
现在你已了解什么是DSL,接下来看看它与你用过的其他编程语言有何不同。
1. DSL与通用编程语言的区别
领域专用语言这个名字其实已经给出了答案。你应该牢记DSL最重要的两个特征:
- 一种DSL专门针对一个特定的问题领域;
- DSL含有建模所需的语法和语义,在与问题域相同的抽象层次对概念建模。
用DSL编程时只需要处理问题域的复杂性,你用不着操心解答域的实现细节和其他非必要因素。(关于非本质复杂性的讨论,参见附录A。)因此,多数情况下非专业程序员也能用好DSL,前提是DSL具备了适当的抽象层次。数学家能轻松学会使用Mathematica进行工作,UI设计师写起HTML来怡然自得,就连硬件工程师都有VHDL(超高速集成电路硬件描述语言,是一种在电子设计自动化即EDA领域使用的DSL)可用,这些都是非专业程序员使用DSL的例子。因为要适应非程序员,DSL必须比通用编程语言更符合用户的直觉。
程序并不是一次写完了事,之后还要维护更新很多年,而其中负责“照料”程序的人很可能并没有参与设计最初的版本。因此,沟通是一个关键问题:程序要有能力与它的目标读者沟通。对于DSL,编译器和CPU都不是它的直接读者,有心理解程序行为的人类大脑才是它的“倾诉对象”。语言要利于交流,要让代码片段能够充分体现出建模者的思考过程。这就要求在设计DSL的时候为语法和语义都找准适合用户的抽象层次。
2. DSL对业务用户的益处
讨论到这里,我们可以总结出DSL区别于一般高级编程语言的两大特质,如下。
DSL给予用户更高层次的抽象。也就是说用户不必分心于具体数据结构的微妙差别等低层次细节,而是专注于解决手头的问题。
DSL只提供有限的语汇,不超出它的领域范围。正因为DSL排除了多余的东西,它能够帮助用户专注于被建模的问题。DSL的“视野”不似通用编程语言那般横向发散 。
对于不懂编程的领域专家,这两种特质使DSL成为了更便利的工具。业务分析员了解的领域,就是DSL抽象出来的领域。
随着越来越多的编程语言支持更高层次的抽象设计,各种DSL正翘首以待成为现今程序开发生态系统的重要组成部分。不懂编程的领域分析师肯定会在其中扮演重要的角色。如果有DSL,分析师从一开始就能撰写正确的测试脚本。测试脚本不是为了立即运行,而是用来保证编写程序时充分考虑到各种可能的业务场景。只要DSL的设计找准了抽象层次,让领域专家直接浏览定义业务逻辑的源代码就不是什么异乎寻常的事情。他们将可以验证业务规则,然后把检查结果直接反馈给开发者。
现在,我们已经了解到DSL可以给开发者和领域用户带来不少好处,下面来认识一下目前已在业界广泛使用的几种领域专用语言。
1.3.2 流行的几种DSL
DSL应用非常广泛。我敢肯定你开发过的每一个应用程序都用到不少DSL,虽然有些不一定被打上DSL的标签。表1-1列举了几种最常用的DSL。
表 1-1 常用的DSL
DSL | 用途 |
---|---|
SQL | 关系型数据库语言,用于查询和变更数据 |
Ant、Rake、Make | 几种用于软件系统构建的语言 |
CSS | 样式表描述语言 |
YACC、Bison、ANTLR | 几种用来生成语法分析器的语言 |
RSpec、Cucumber | Ruby环境下的行为驱动测试语言 |
HTML | 用于Web的标记语言 |
你平时经常用到的DSL肯定不止这些。你能分辨出它们有什么共同特征吗?来看下面几点。
- 所有DSL都有对应专门的领域。每种语言都拥有“有限表达力”(limited expressivity),而你只能用一种DSL解决其特定领域的问题。你不可能只用HTML搭建出一套货运管理系统。
定义 马丁·福勒(Martin Fowler)用“有限表达力”来描述DSL最重要的特征。在2009年DSL开发者会议的主题演讲 (参见1.9节文献[3])上,他提出有限表达力是DSL与通用编程语言的根本区别。通用编程语言可以给任何事物建模,而一种DSL只能给一个专门领域建模,可是表现力更强。
使用表1-1列出的语言(以及其他被广泛使用的语言),一般即使用它们所建立的抽象。在绝大多数情况下,你并不需要了解语言的底层实现。每种DSL都提供了一套供你搭建解答域模型的契约,为了搭建更复杂的模型可以把不同的契约组合起来,但终归不需要跨出契约的范围和深入DSL的实现层次。
任何一种DSL都具备充分的表达能力,足以使不懂编程的用户理解程序的意图。DSL并非给开发者提供的一套API而已,它的每一个API都以领域语汇精炼地表达丰富的含义。
用任何一种DSL编写的源代码文件,即使数月之后再重新翻看,你也可以立即领会当初的意思。
事实证明,依托DSL进行开发更能鼓励开发者与领域专家进行更好的交流。这是其很重要的优点。借助DSL,不擅长编程的领域专家不必勉强转变为一般程序员。得益于DSL的表现力和其特意以沟通为目的提供的API,领域专家可以理解解答域的抽象实现了哪些业务规则,以及其实现是否充分覆盖了所有可能的业务场景。
我们来看一段有启发意义的Rakefile代码片段,示例中所用的Rake也是表1-1所列的一种DSL,主要用于构建Ruby语言编写的系统:
desc "Default Task"
task :default => [ :test ]
Rake::TestTask.new { |t|
t.libs << "test"
t.pattern = 'test/*_test.rb'
t.verbose = true
t.warning = false
}
这段代码建立一系列单元测试,并把它们作为默认任务来运行。即使你不懂Ruby,也不会看错这段代码的意思;它的表达能力没有受影响。这是怎么做到的?它各处重要部分的用词正好匹配了你所熟悉的语汇,而且给DSL使用者提供了简单明了的界面 。Rake的使用者是软件开发人员,所以这种语言将其语义设定到符合开发者预期和理解力的抽象层次。同样,如果打算开发一种给金融交易员群体使用的DSL,你必须谨记使表现力层次符合交易台上的人的预期和经验。这里的附加内容简要说明交易系统的一些基本术语;请了解一下相关定义,因为这些概念会反复出现在本书所用的DSL示例中。
金融中介系统:交易和结算
交易在两方(交易双方)之间进行,遵照交易市场的规章进行证券与现金之间的互换。交易只是承诺,需要在交易发生后的规定天数内完成结算。进行结算的日期称为结算日,根据若干因素确定,如实行交易的具体市场、证券的生命周期、交易的性质、实行交易的日期(交易日)等。
每一笔交易都对应一个现金价值。现金价值是购买证券的一方应付出的金钱数量。现金价值取决于若干因素,例如基本价值、印花税、经纪费用和佣金等。
交易在证券交易所成交后,其详情被输入到交易机构的后台完成一个交易充实过程,由交易系统计算出所有的细节事项:结算日、交易税、佣金和最终的现金价值。
设计DSL的时候,你要时刻把使用者放在心上。DSL的表现力和粒度要尽力满足用户理解的需要。你会在后面的章节中学习如何在用户感觉最自然的抽象层次上设计DSL。现在我们先来考虑DSL怎样更好地在问题域和解答域之间建立映射,填补图1-2缺失的一些环节,使你对此有更全面的认识。
1.3.3 DSL的结构
图1-3展现了DSL脚本怎样将共通语汇联系到解答域的实现模型。
图1-3 DSL脚本将实现模型表示为领域语言。脚本中的用词都出自共通语汇,使用户对语言感觉更自然
设计得当的DSL应该体现以下三项原则,以便与领域用户更好地“沟通”。
DSL要为问题域制品提供直接的映射。如果问题域有一个名为Trade的实体,那么DSL脚本就必须包含同样名称同样角色的一个抽象。
DSL脚本必须使用问题域的共通语汇。这些语汇将成为开发者与业务用户增进交流的催化剂。如图1-3所示,当业务用户与软件中的领域模型交互的时候,DSL脚本就是他们的用户界面。
DSL脚本必须对底层实现进行抽象。这是抽象设计的一项重要原则,对于DSL的设计同样适用。DSL脚本中不可以出现因为实现细节而引入的非本质复杂性 。1
1 非本质复杂性(accidental complexity),与本质复杂性(essential complexity)相对应,指程序开发过程中出现的、与问题本身无关的复杂性。本质复杂性是与生俱来不可避免的,而非本质复杂性却是由于解决问题的手段而产生的。——译者注
在图1-3中,“DSL脚本”节点与其他节点的联系即为以上三项原则的形象表示。只要在设计中牢记这些原则,你所设计的DSL就能充分发挥与领域用户“沟通”的效果。下一节将讲述DSL的执行模型——当用户运行软件时DSL脚本及其实现模型是如何呈现给用户的。