第一章 第1章 入门例子
第三节 为格兰特小姐的控制器编写程序
对于编写控制器软件的人而言,这种简单性意味着更容易理解,并且开发人员之外的人也可以理解这种行为。搭建系统的人能够查看这段代码,并且理解它的运作方式,虽然他们无法理解控制器本身的Java代码。即便他们只读了DSL,但对于指出错误或者同Java开发人员进行有效的交流来说,这就足够了。DSL扮演领域专家和业务分析人员之间的交流媒介,虽然构建这种DSL也存在一些实际的困难,但能够在软件开发最困难的交流鸿沟上架起一座桥梁,其益处也让 这种尝试物有所值。
现在,回顾一下XML的表示形式。这是一种DSL吗?我想说,它是。只不过它是用XML的语法载体而已─ 但是它依旧是DSL。这个例子引出了一个设计问题:哪种做法更好:为DSL定制语法,还是使用XML语法?XML更易于解析,因为人们已经熟悉了解析XML。(然而,同为定制语法编写解析器相比,为XML编写解析器花了我同样多的时间。)我要声明一点,定制语法易读得多,至少在这个例子里是这样的。然而,回顾一下这个选择,我们会发现,DSL核心部分的权衡也是一样的。的确,我们可以认为,大多数XML配置文件本质上都是DSL。
看看下面这段代码,它看上去像是这个问题的DSL吗?
event :doorClosed, "D1CL"
event :drawerOpened, "D2OP"
event :lightOn, "L1ON"
event :doorOpened, "D1OP"
event :panelClosed, "PNCL"
command :unlockPanel, "PNUL"
command :lockPanel, "PNLK"
command :lockDoor, "D1LK"
command :unlockDoor, "D1UL"
resetEvents :doorOpened
state :idle do
actions :unlockDoor, :lockPanel
transitions :doorClosed => :active
end
state :active do
transitions :drawerOpened => :waitingForLight,
:lightOn => :waitingForDrawer
end
state :waitingForLight do
transitions :lightOn => :unlockedPanel
end
state :waitingForDrawer do
transitions :drawerOpened => :unlockedPanel
end
state :unlockedPanel do
actions :unlockPanel, :lockDoor
transitions :panelClosed => :idle
end
同之前的定制语言相比,它稍微有些噪音,但依旧相当清晰。与我有相近语言偏好的人可能看出来了,这是Ruby。在创建更可读的代码方面,Ruby给了我许多语法上的支持,因此,我可以使它很像一门定制语言。
Ruby开发人员会把这段代码当做一种DSL。我用到的是Ruby这方面能力的一个子集,表现的想法同使用XML和定制语法是一样的。从本质上说,我是把DSL嵌入Ruby里,用Ruby的子集作为我的语法。从某种程度上来说,这更多地取决于态度,而非其他什么。我选择透过DSL眼镜观看Ruby代码。但这是一个具有长期传统的观点─Lisp程序员通常会考虑在Lisp里创建DSL。
在此,我要指出,文本DSL有两种,称为外部DSL(external DSL)和内部DSL(internal DSL)。外部DSL是指,在主程序设计语言之外,用一种单独的语言表示领域专用语言。这种语言用的可能是定制语法,或者遵循另一种表现的语法,比如XML。内部DSL是指,用通用语言的语法表示的DSL。这种做法就是出于领域专用的目的,而按照某种风格来使用这种语言。
也许有人还听说一个术语,嵌入式DSL(embedded DSL),它是内部DSL的同义词。虽然这个术语应用得相当广泛,但我还是会避免使用它,因为“嵌入式语言”(embedded language)同样适用于在应用中嵌入的脚本语言,比如Excel里的VBA,Gimp里的Scheme。
回过头来考虑一下原来的Java配置代码。它是一种DSL吗?我想说,不是。这段代码感觉像是同API缝合在一起的,而上面的Ruby代码则更有声明式语言的感觉。这是否意味着无法用Java实现内部DSL呢?下面这段代码怎么样?
public class BasicStateMachine extends StateMachineBuilder {
Events doorClosed, drawerOpened, lightOn, panelClosed;
Commands unlockPanel, lockPanel, lockDoor, unlockDoor;
States idle, active, waitingForLight, waitingForDrawer, unlockedPanel;
ResetEvents doorOpened;
protected void defineStateMachine() {
doorClosed. code("D1CL");
drawerOpened. code("D2OP");
lightOn. code("L1ON");
panelClosed.code("PNCL");
doorOpened. code("D1OP");
unlockPanel.code("PNUL");
lockPanel. code("PNLK");
lockDoor. code("D1LK");
unlockDoor. code("D1UL");
idle
.actions(unlockDoor, lockPanel)
.transition(doorClosed).to(active)
;
active
.transition(drawerOpened).to(waitingForLight)
.transition(lightOn). to(waitingForDrawer)
;
waitingForLight
.transition(lightOn).to(unlockedPanel)
;
waitingForDrawer
.transition(drawerOpened).to(unlockedPanel)
;
unlockedPanel
.actions(unlockPanel, lockDoor)
.transition(panelClosed).to(idle)
;
}
}
虽然这段代码格式上有些奇怪,而且用到了一些不常见的编程约定,但它确实是有效的Java。这段代码我愿意称为 DSL;虽然同Ruby DSL相比,它有些乱,但它还是有DSL所需的声明流。
是什么让内部DSL不同于通常的API呢?这是一个很难回答的问题,稍后,在4.1节,我会花更多的时间来讨论,但它会归结为一种流,只不过用的是一种类语言的模糊记法而已。
也许,有人还碰到过内部DSL的另一个术语,连贯接口(fluent interface)。这个术语强调这样一个事实:内部 DSL实际上只是某种形式的API,只不过其设计考虑到了连贯性难以琢磨的质量。鉴于这种差别,最好给非连贯 API一个名字(我用的术语是,命令)查询API(command–query API)。