2.5 DSL的生命周期
为了介绍DSL,开篇先描述一个框架,及其命令–查询API,基于这个API,构建一层DSL以简化操作。我用这种方式,是因为我觉得这种方式有助于理解DSL,但这并不是人们在实际中使用DSL的唯一方式。
另一种常见的方式是先定义DSL。在这种模式下,可以先从一些场景开始,按照期望DSL的样子,把这些场景写下来。如果语言是业务功能的一部分,最好和领域专家一起做─这是一个好的开始,使用DSL作为一种沟通媒介。
有人喜欢从语句开始,他们期待这些语句能够语法正确。这意味着,对内部DSL,要符合宿主语言的语法; 对外部 DSL,语句要能够解析。其他DSL开始时比较非正式,然后再对照DSL进行修改,以得到一种合理的语法。
以这种方式实现状态机,我们要和了解客户需求的人一起坐下来,想出一套关于控制器行为的例子,这些例子可以基于人们过去的需求,也可以基于我们对他们期望的理解。对于每个例子,尝试用DSL的形式把它们写下来。随着处理到不同的情况,我们就要修改DSL,支持新的能力。最后,我们就会得到一套合理的用例,以及对这些用例的伪DSL描述。
如果用的是语言工作台,就要在工作台之外完成这一阶段,用一个纯文本编辑器,或者一个普通的绘图软件,也可以是纸和笔。
一旦有了一套典型的伪DSL,就可以着手实现它了。这里的实现包括以宿主语言设计的状态机模型、 模型的命令–查询API、DSL的具体语法以及DSL和命令–查询API之间的转换。实现方式有很多种。有人喜欢一次做一点,横跨所有元素:构建少量模型、添加DSL驱动模型,然后用测试把所有东西串起来;也有人倾向于先构建和测试整个框架,然后在其上构建一层DSL;还有人喜欢先准备好DSL,然后构建程序库,再使它们适配。作为一个增量主义者,我倾向于端到端地实现薄薄几层的功能,因此,我会采用三种做法中的第一种。
我会从我所见的最简单的一个用例出发。采用测试驱动开发的方式,编写一个支持这个用例的程序库。然后,通过DSL实现这个用例,把它同之前构建的框架连起来。我很乐于对DSL做出一些修改,使其易于修改,当然,我会拿着这些修改与领域专家沟通,确保我们对于共同的沟通媒介有同样的理解。做完一个,我会继续做下一个。就这样,我逐渐演化着框架、测试优先,然后逐渐演化着DSL。
这并不是说模型优先的路线是一个糟糕的选择,事实上,这种做法往往可以做得很漂亮。模型优先常常发生在一开始没有考虑DSL,或者不确定是否需要DSL的情况下。这时,我们会先构建框架,用了一段时间之后,我们觉得DSL是一 个有益的补充。在这个例子里,已经有一个可用的状态机模型,而且许多客户已经开始使用。这时,我们意识到,要增加一个新客户困难重重,因此决定尝试DSL。
基于模型发展DSL的方法有两种。对于“语言生长”(language–seeded)的方式,要慢慢地在模型之上构建DSL,把模型几乎视为黑盒。首先看看目前所有的控制器,然后草拟出每个控制器的伪DSL。然后,就像前面提及的情况那样,一个场景一个场景地实现DSL,通常,我们不会对模型做任何深入的修改,尽管给模型添加一些方法能够更好地支持DSL。
对于“模型生长”(model–seeded)的方式,要先给模型加入一些连贯方法(fluent method),让模型更易于配置,然后逐渐把它们抽成DSL。这种方式更适用于内部DSL,可以视之为模型的一次重量级重构,派生出内部DSL。“模型生长”的方式最吸引人的方面在于,它是逐步进行的,构建DSL并不需要显著的成本。
当然,在很多情况下,我们甚至连框架都没有。写了几个控制器之后,我们才意识到,有很多通用功能。然后,就会重构系统,拆分模型与控制代码。这个拆分是至关重要的一步。虽然做这件事时,脑海中已经有了DSL,但我依然倾向于首先完成拆分,然后在其上构建DSL。
至此,我要强调一件事,希望我的担心是多余的。一定要保证所有的DSL脚本都保存在某种形式的版本控制系统里。DSL脚本是代码的一部分,所以,就像其他东西一样,它也应该放到版本控制里。文本化DSL的一大优势在于,它们可以很好地与版本控制系统协作,也就很容易跟踪系统行为的变化。