您现在的位置:e-works > 智造书屋 > 书籍列表 > 领域特定语言 > 为格兰特小姐的控制器编写程序

第一章 第1章 入门例子

第三节 为格兰特小姐的控制器编写程序

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

一个用于多种配置的程序库

图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>
    大多数读者对这种表现形式应该更熟悉一些:表现为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的简单性意味着,它是易于编辑和处理的。