《领域特定语言》一1.3 为格兰特小姐的控制器编写程序

1.3 为格兰特小姐的控制器编写程序

至此,我们已经实现了状态机模型,接下来,就可以为格兰特小姐的控制器编写程序了,如下所示:

Event doorClosed = new Event("doorClosed", "D1CL");
Event drawerOpened = new Event("drawerOpened", "D2OP");
Event lightOn = new Event("lightOn", "L1ON");
Event doorOpened = new Event("doorOpened", "D1OP");
Event panelClosed = new Event("panelClosed", "PNCL");

Command unlockPanelCmd = new Command("unlockPanel", "PNUL");
Command lockPanelCmd = new Command("lockPanel", "PNLK");
Command lockDoorCmd = new Command("lockDoor", "D1LK");
Command unlockDoorCmd = new Command("unlockDoor", "D1UL");

State idle = new State("idle");
State activeState = new State("active");
State waitingForLightState = new State("waitingForLight");
State waitingForDrawerState = new State("waitingForDrawer");
State unlockedPanelState = new State("unlockedPanel");

StateMachine machine = new StateMachine(idle);

idle.addTransition(doorClosed, activeState);
idle.addAction(unlockDoorCmd);
idle.addAction(lockPanelCmd);

activeState.addTransition(drawerOpened, waitingForLightState);
activeState.addTransition(lightOn, waitingForDrawerState);

waitingForLightState.addTransition(lightOn, unlockedPanelState);
waitingForDrawerState.addTransition(drawerOpened, unlockedPanelState);

unlockedPanelState.addAction(unlockPanelCmd);
unlockedPanelState.addAction(lockDoorCmd);
unlockedPanelState.addTransition(panelClosed, idle);

machine.addResetEvents(doorOpened);

经过查看上述代码,我们发现,它与之前的代码有着很大的不同。之前的代码描述了如何构建状态机模型,而上面这段代码则是在配置一个特定的控制器。我们常常会看到这样一种划分:一方面是程序库(见图1-3)、框架或者组件的实现代码;另一方面是配置代码或组件组装代码。从本质上说,这种做法分开了公共代码和可变代码。用公共代码构建一套组件,然后根据不同的目的进行配置。

配置代码还有另外一种表现形式:

<stateMachine start = "idle">
 <event name="doorClosed" code="D1CL"/>
 <event name="drawerOpened" code="D2OP"/>
 <event name="lightOn" code="L1ON"/>
 <event name="doorOpened" code="D1OP"/>
 <event name="panelClosed" code="PNCL"/>

 <command name="unlockPanel" code="PNUL"/>
 <command name="lockPanel" code="PNLK"/>
 <command name="lockDoor" code="D1LK"/>
 <command name="unlockDoor" code="D1UL"/>

 <state name="idle">
  <transition event="doorClosed" target="active"/>
  <action command="unlockDoor"/>
  <action command="lockPanel"/>
 </state>

 <state name="active">
  <transition event="drawerOpened" target="waitingForLight"/>
  <transition event="lightOn" target="waitingForDrawer"/>
 </state>

 <state name="waitingForLight">
  <transition event="lightOn" target="unlockedPanel"/>
 </state>

 <state name="waitingForDrawer">
  <transition event="drawerOpened" target="unlockedPanel"/>
 </state>

 <state name="unlockedPanel">
  <action command="unlockPanel"/>
  <action command="lockDoor"/>
  <transition event="panelClosed" target="idle"/>
 </state>

 <resetEvent name = "doorOpened"/>
</stateMachine>

大多数读者对这种表现形式应该更熟悉一些:表现为XML文件。这种做法有几个好处。第一个明显的好处是,无须为每个要实现的控制器编译一个单独的Java程序,相反,只要把状态机组件加上相应的解析器编译到一个公共的JAR 里,然后,发布对应的XML文件,当状态机启动时读取这个文件。控制器行为的任何修改都无须发布新的JAR。当然,我们需要为此付出一些代价,许多配置上的语法错误只能在运行时检测出来,虽然各种各样的XML模式系统还能帮上点忙。我还是“广泛测试”(extensive testing)的超级粉丝,广泛测试不仅可以在编译时检查就捕获到大多数错误,还可以发现一些类型检查无法确定的致命问题。有了这种及时测试,就不必担心把错误检测带到运行时。
第二个好处在于文件本身的表现力。我们不必再去考虑通过变量进行连接的细节。相反,我们拥有了一种声明方式,从许多方面来看,这种方式读起来都会更加清晰。这里还有一些限制:这个文件只能表示 配置─这种限制也是有益的,因为它会降低人们编写组装组件代码犯错的概率。
或许,你听说过声明式编程这回事。对我们而言,更常见的模型是命令式(imperative)模型,也就是,用一系列的步骤指挥计算机。“声明式”是一个非常模糊的术语,但是,它通常适用于所有远离了命令式编程的方式。在这里,我们向那个方向迈进了一步:远离变量倒换,用XML的子元素表示状态内的动作和转换。
正是有了这些好处,如此之多的Java和C#的框架才采用XML配置文件配置。现如今,有时我们会觉得自己是在用XML 编写程序,而非自己的主编程语言。
下面是配置代码的另一个版本:

events
 doorClosed D1CL
 drawerOpened D2OP
 lightOn   L1ON
 doorOpened D1OP
 panelClosed PNCL
end

resetEvents
 doorOpened
end

commands
 unlockPanel PNUL
 lockPanel  PNLK
 lockDoor  D1LK
 unlockDoor D1UL
end

state idle
 actions {unlockDoor lockPanel}
 doorClosed => active
end

state active
 drawerOpened => waitingForLight
 lightOn  => waitingForDrawer
end

state waitingForLight
 lightOn => unlockedPanel
end

state waitingForDrawer
 drawerOpened => unlockedPanel
end

state unlockedPanel
 actions {unlockPanel lockDoor}
 panelClosed => idle
end

这确实是代码,虽然使用的不是我们所熟悉的语法。实际上,这是一种定制语法,专为这个例子而打造的。相比于XML语法,我认为这种语法更易写,最重要的是,更易读。它更简洁,省却了许多引号和噪音字符,这些是用XML所要忍受的。或许,你的做法不尽相同,但重点在于,我们可以构造自己和团队所喜欢的语法。我们依然可以在运行时加载(就像XML一样),但是,这么做不是必需的(XML也不必这么做),如果想在编译时加载的话。
这样的语言就是领域专用语言,它有着DSL的许多特征。首先,它只适用于非常有限的用途─除了配置这种特定的状态机外,它什么都做不了。这样带来的结果就是,这个DSL非常简单─它没有控制结构,也没有其他的东西。它甚至不是图灵完备的。用这种语言不能编写整个应用,它所能做的只是描述应用一个小的方面。这样做的结果就是,DSL只有同其他语言配合起来,才能完成整个工作。但是,DSL的简单性意味着,它是易于编辑和处理的。
对于编写控制器软件的人而言,这种简单性意味着更容易理解,并且开发人员之外的人也可以理解这种行为。搭建系统的人能够查看这段代码,并且理解它的运作方式,虽然他们无法理解控制器本身的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)。

时间: 2024-09-20 17:10:04

《领域特定语言》一1.3 为格兰特小姐的控制器编写程序的相关文章

《领域特定语言》一导读

前 言 在我开始编程之前,DSL(Domain–Specific Language,领域特定语言)就已经成了程序世界中的一员.随便找个UNIX或者Lisp老手问问,他一定会跟你滔滔不绝地谈起DSL是怎么成为他的镇宅之宝的,直到你被烦得痛不欲生为止.但即便这样,DSL却从未成为计算领域的一大亮点.大多数人都是从别人那里学到DSL,而且只学到了有限的几种技术. 我写这本书就是为了改变这个现状.我希望通过本书介绍的大量DSL技术,让你有足够的信息来做出决策:是否在工作中使用DSL,以及选择哪一种DSL

《领域特定语言》一第1章 入 门 例 子1.1 哥特式建筑安全系统

第1章 入 门 例 子 落笔之初,我需要快速地解释一下本书的内容,就是解释什么是领域特定语言(Domain– Specific Language,DSL).为达此目的,我一般都会先展示一个具体的例子,随后再给出抽象的定义.因此,我会从一个例子开始,展示DSL可以采用的不同形式.在第2章里,我会试着把这个定义概括为一些更广泛适用的东西. 1.1 哥特式建筑安全系统 在我的童年记忆里,电视上播放的那些低劣的冒险电影是模糊却持久的.通常,这些电影的场景会安排某个古旧的城堡.密室或走廊在其中起着重要的作

如何设计一门编程语言(十) 正则表达式与领域特定语言(DSL)

几个月前就一直有博友关心DSL的问题,于是我想一想,我在gac.codeplex.com里面也创建了一些DSL,于是今天就来说一说这个事情. 创建DSL恐怕是很多人第一次设计一门语言的经历,很少有人一开始上来就设计通用语言的.我自己第一次做这种事情是在高中写这个傻逼ARPG的时候了.当时做了一个超简单的脚本语言,长的就跟汇编差不多,虽然每一个指令都写成了调用函数的形态.虽然这个游戏需要脚本在剧情里面控制一些人物的走动什么的,但是所幸并不复杂,于是还是完成了任务.一眨眼10年过去了,现在在写Gac

《领域特定语言》一第2章 使用DSL 2.1定义DSL

第2章 使用DSL 看过上一章的例子后,即便尚未给出DSL的一般定义,对于何为DSL,你也应该已经有了自己的认识.(第10章中有更多例子.)现在,要开始讨论DSL的定义及其优势与问题.这样就可以为下一章讨论DSL实现提供一些上下文. 2.1定义DSL "领域特定语言"是一个很有用的术语和概念,但其边界很模糊.一些东西很明显是DSL,但另一些可能会引发争议.这一术语由来已久,不过,正如软件行业中的很多东西一样,它也从未有过一个确切的定义.然而,就本书而言,定义是非常有价值的.领域特定语言

《领域特定语言》一1.5使用代码生成

1.5使用代码生成 在迄今为止的讨论中,要处理DSL,组装"语义模型"(第11章),然后执行语义模型,提供我们希望从控制器得到的行为.在语言圈子里,这种方式称为解释(interpretation).在解释文本时,会先解析文本,然后程序立刻产生结果.(在软件圈子里,解释是个棘手的词语,它承载了太多含义,然而,这里严格限制为立即执行的形式.)在语言领域里,与解释相对的是编译.在编译(compilation)时,先解析程序文本,产生中间输出,然后单独处理输出,提供预期行为.在DSL的上下文里

《领域特定语言》一3.6 测试DSL

3.6 测试DSL 过去二十年,我变得越来越不想谈论测试.我已然成为一个忠实粉丝,迷恋着测试驱动开发 [Beck TDD]以及类似的技术:将测试置于程序设计之前.所以,我已无法脱离测试思考DSL.对DSL而言,我把其测试分为三个独立的部分:"语义模型"(第11章)的测试,解析器的测试,以及脚本的测试. 3.6.1语义模型的测试 我首先想到的部分是"语义模型"(第11章)的测试.这些测试用来保证语义模型能够如预期般工作,也就是说,当执行模型时,根据编写的代码,它能够产

《领域特定语言》一1.2 状态机模型

1.2 状态机模型 一旦团队达成共识,认为对于指定控制器如何运作而言,状态机是一个恰当的抽象,那么,下一步就是确保这个抽象能够运用到软件自身.如果人们在考虑控制器行为时,也要考虑事件.状态和转换,那么,我们希望这些词汇也可以出现在软件代码里.从本质上说,这就是领域驱动设计(Domain–Driven Design)中的Ubiquitous Language [Evans DDD] 原则,也就是说,我们在领域人员(那些描述建筑安全该如何运作的人)和程序员之间构建的一种共享语言.对于Java程序来说

《领域特定语言》一2.2为何需要DSL

2.2为何需要DSL 至此,我希望,对什么是DSL,我们已经有了一个很好的共识,接下来的问题是,为何要考虑采用DSL.DSL只是一种工具,关注点有限,无法像面向对象编程或敏捷方法论那样,引发软件开发思考方式的深刻变革.相反,它是在特定条件下有专门用途的一种工具.一个普通的项目可能在多个地方采用了多种DSL─事实上很多项目这么做了.在1.4节中,一直强调,DSL只是模型的一个薄壳,这个模型可能是程序库,也可能是框架.这句话提醒我们,当考虑DSL的优劣时,一定要分清它是来自DSL的模型,还是DSL本

《领域特定语言》一2.4 广义的语言处理

2.4 广义的语言处理 本书是关于领域专用语言的,但它也涉及语言处理技术.之所以二者重合,是因为在普通的开发团队里,用到语言处理技术的情况,90%都是为了DSL.但是,这些技术也可以用于其他方面,若不讨论这些情况,将是我的失职.我曾遇到过这方面一个很好的例子,那是在一次拜访ThoughtWorks项目团队时.他们有一个任务,要与某第三方系统通信,发送的消息以COBOL copybook定义.COBOL copybook是一种用来描述记录的数据结构格式.因为系统中有很多地方要用到,所以我的同事Br