1.4 语言和语义模型
在这个例子之初,我谈到了构建一个状态机模型。这种模型的存在,以及它同DSL的关系,是至关重要的。在这个例子里,DSL的角色就是组装状态机模型。因此,当解析定制语法的版本时,遇到:
events
doorClosed D1CL
会创建一个新的事件对象(new Event("doorClosed","D1CL")),把它保存在一边(在一个 “符号表”(第14章)里),这样,遇到doorClosed=>active时,就可以将它包含在一个转换里(使用addTransition)。这个模型就是个引擎,它提供了状态机的行为。事实上,可以说,这个设计的能力大多源自这样一个模型。如图1-4所示,DSL所做的一切就是提供一种更可读的方式来组装这个模型─这就是与开始的命令查询API不同的地方。
从DSL的角度来看,我把这个模型称为“语义模型”(第11章)。谈及编程语言时,我们常常会提及语法(syntax)和语义(semantics)。语法描述程序的合法表达式,而在定制语法的DSL里所能描述的一切是由文法(grammar)决定的。程序的语义是指,它代表着什么,也就是说,当执行时,它能做什么。在这个例子里,模型定义了语义。如果你习惯使用Domain Model [Fowler PoEAA] ,这里就可以认为语义模型是与之非常类似的东西。
(你可以查看一下“语义模型”(第11章),了解一下语义模型同Domain Model之间的差异,还有语义模型和抽象语法树之间的不同。)
我有一个观点,对于一个设计良好的DSL而言,语义模型至关重要。在实际中,有的DSL用语义模型,而有些没有,但是我强烈建议,几乎始终(almost always)应该使用语义模型。(我发现,当说一些词比如“always”时,如果不加上限定词“almost”,几乎是不可能的。我几乎还没有找到一条广泛适用的规则。)
我提倡使用语义模型,因为它清晰地将语言解析和结果语义的关注点切分开。我可以推究出状态机的运作机制,对状态机进行增强和调试,而无须顾及语言问题。通过命令–查询接口,就可以组装状态机测试模型。状态机模型和DSL可以独立演进,即便还没想好如何通过语言表示,依然可以为模型添加新特性。也许,最关键的点在于,模型可以独立测试,而无须涉及语言。确实,上面所有DSL的例子都构建在相同语义模型上,基于这个模型,可以创建出完全相同的配置对象。
在这个例子里,语义模型是对象模型。语义模型还可以有其他形式。即便它只是一个纯粹的数据结构,所有的行为 都在单独的函数里,我依然愿意称之为语义模型,因为在那些函数的上下文里,数据结构表现出了DSL脚本特定的含义。
从这个角度来看,DSL只是扮演着展示模型配置机制的角色。使用这种方式的益处大多源自模型,而非DSL。为客户配置新的状态很容易,这是模型的属性,而非DSL。控制器可以在运行时改变,无须编译,这是模型的特性,而非DSL。可以在控制器的多次安装中重用代码,这是模型的属性,而非DSL。由此可见,DSL只是模型的一个薄薄的“门面”(facade)。
模型提供了诸多益处,与DSL如何表现无关。因此,我们一直使用它们。通过使用程序库和框架,我们可以明智地回避一些工作。在我们自己的软件里,构建模型,增进抽象,这样,就可以更快地开发。无论是作为程序库或者框架发布,或只是为自己的代码服务,即便没有任何可见的DSL,良好的模型都可以运作良好。
不过,DSL可以增强模型的能力。正确的DSL让我们更容易理解一个特定状态机的运作机制。一些DSL甚至可以让我们 在运行时配置模型。因此,DSL是对模型的一个有益补充。
DSL所带来的益处与状态机紧密相关,其所组成的某个特定模型就扮演了系统程序的角色。要改变状态机的行为,就需要修改模型中的对象及其相互关系。这种风格的模型通常称为“适应性模型”(第47章)。这样得到的是一个模糊了代码和数据之间差异的系统,只看代码,是无法理解状态机行为的,还必须了解对象实例的连接方式。当然,从某种程度上 说,这总是对的,任何程序对不同的数据都会给出不同的结果,但在此有个极大的差异,因为状态对象的存在会在很大程度上改变系统的行为。
适应性模型非常强大,但是通常也很难用,因为人们看不到任何定义特定行为的代码。DSL是有价值的,它提供了一种显式的方式表现代码,这种形式让人们对状态机编程有了感觉。
状态机可以很好地适用于适应性模型,原因在于,它是另一种计算模型。常规的编程语言提供了一种为机器编程的标准思考方式,多数情况下它运作良好。但是,有时,我们需要一些不同的方式,比如“状态机”(第51章),“产生式规则系统”(第50章),或者“依赖网络”(第49章)。使用适应性模型是一种好的方式,它提供了另一种计算模型,DSL则简化了为这种模型编程的方式。本书稍后会描述“其他一些计算模型”(第7章),在那里,你会了解到它们是什么样子,以及如何实现。或许你曾听说,有人把这种使用DSL的方式称为声明式编程。
在讨论这个例子时,我采用的流程是:首先构建模型,然后在此基础之上,用DSL封装出一个层次,对其进行操作。之所以用这种方式进行描述,是因为我觉得这是一种简单的方式,有助于理解DSL如何用于软件开发。虽然模型优先的情况很常见,但它并不是唯一方式。在不同的场景下,我们可能会与领域专家交谈,假定他们可以理解状态机方式。稍后,我们和他们一起工作,创建出他们可以理解的DSL。在这种情况下,DSL和模型可以同步构建。