2.2为何需要DSL
至此,我希望,对什么是DSL,我们已经有了一个很好的共识,接下来的问题是,为何要考虑采用DSL。
DSL只是一种工具,关注点有限,无法像面向对象编程或敏捷方法论那样,引发软件开发思考方式的深刻变革。相反,它是在特定条件下有专门用途的一种工具。一个普通的项目可能在多个地方采用了多种DSL─事实上很多项目这么做了。
在1.4节中,一直强调,DSL只是模型的一个薄壳,这个模型可能是程序库,也可能是框架。这句话提醒我们,当考虑DSL的优劣时,一定要分清它是来自DSL的模型,还是DSL本身。经常有人将二者混淆。
DSL有其自身的价值。当考虑采用DSL时,要仔细衡量它的哪些价值适合于我们的情况。
2.2.1 提高开发效率
DSL的核心价值在于,它提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。拿格兰特小姐控制器的定义来说,相比于采用命令–查询API,DSL形式对我们而言更容易理解。
这种清晰并非只是审美追求。一段代码越容易看懂,就越容易发现错误,也就越容易对系统进行修改。因此,我们鼓励变量名要有意义,文档要写清楚,代码结构要写清晰。基于同样的理由,我们应该也鼓励采用DSL。
人们经常低估缺陷对生产率的影响。缺陷不仅损害软件的外部质量,还浪费开发人员的时间去调查以及修复,降低开发效率,并使系统的行为异常,播下混乱的种子。DSL的受限表达性,使其难于犯错,纵然犯错,也易于发现。
模型本身可以极大地提升生产率。通过把公共代码放在一起,它可以避免重复。首先,它提供了一种“用于思考问题”的抽象,这样,更容易用一种可理解的方式指定系统行为。DSL提供了一种“对阅读和操作抽象”更具表达性的形式,从而增强了这种抽象。DSL还可以帮助人们更好地学习使用API,因为它将人们的关注点转移到怎样将API方法整合在一起。
我还遇到过一个有趣的例子,使用DSL封装一个棘手的第三方程序库。当命令–查询接口设计得很糟糕时,DSL 惯常的连贯性就得以凸现。此外,DSL只须支持客户真正用到的部分,这大大降低了客户开发人员学习的成本。
2.2.2与领域专家的沟通
我相信,软件项目中最困难的部分,也是项目失败最常见的原因,就是开发团队与客户以及软件用户之间的沟通。DSL提供了一种清晰而准确的语言,可以有效地改善这种沟通。
相比于关于生产率的简单争论,改善沟通所带来的好处显得更加微妙。首先,很多DSL并不适用于沟通领域问题,比如,用于正则表达式或构建依赖关系的DSL,在这些情况下就不合适。只有一部分独立DSL确实应用这种沟通手段。
当在这样的场景下讨论DSL时,经常会有人说:“好吧,现在我们不需要程序员了,领域专家可以自己指定业务规则。”我把这种论调称为“COBOL谬论”─因为COBOL曾被人寄予这样的厚望。这种争论很常见,不过,我觉得这种争论不值得在此重复。
虽然存在“COBOL谬论”,我仍然觉得DSL可以改善沟通。不是让领域专家自己去写DSL,但他们可以读懂,从而理解系统做了什么。能够阅读DSL代码,领域专家就可以指出问题所在。他们还可以同编写业务规则的程序员更好地交流,也许,他们还可以编写一些草稿,程序员们可以将其细化成适当的DSL规则。
但我不是说领域专家永远不能编写DSL。我遇见过很多团队,他们成功地让领域专家用DSL编写了大量系统功能。但我仍然认为,使用DSL的最大价值在于,领域专家能够读懂。所以编写DSL的第一步,应该专注于易读性,这样即便后续的目标达不到,我们也不会失去什么。
使用DSL是为了让领域专家能够看懂,这就引出了一个值得争议的问题。如果希望领域专家理解一个“语义模型”(第11章)的内容,可以将模型可视化。这时就要考虑一下,相比于支持一种DSL,是不是只使用可视化会是一种更有效的办法。可视化对于DSL而言,是一种有益的补充。
让领域专家参与构建DSL,与让他们参与构建模型是同样的道理。我发现,与领域专家一起构建模型能够带来很大的好处,在构建一种Ubiquitous Language [Evans DDD] 的过程中,程序员与领域专家之间可以深入沟通。DSL提供了另一种增进沟通的手段。随着项目的不同,我们可能发现,领域专家可能会参与模型和DSL,也可能只参与 DSL。
实际上,有些人发现,即便不实现DSL,有一种描述领域知识的DSL,也能带来很大的好处。即使只把它当做沟通平台也可以获益。
总的来说,让领域专家参与构建DSL比较难,但回报很高。即使最终不能让领域专家参与,但是开发人员在生产率方面的提升,也足以让我们大受裨益,因此,DSL值得投入。
2.2.3执行环境的改变
当谈及将状态机表述为XML的理由时,一个重要的原因是,状态机定义可以在运行时解析,而非编译时。在这种情况下,我们希望将代码运行于不同的环境,这类理由也是使用DSL一个常见的驱动力。对于XML配置文件而言,将逻辑从编译时移到运行时就是一个这样的理由。
还有一些需要迁移执行环境的情况。我曾见过一个项目,它要从数据库里找出所有满足某种条件的合同,给它们打上标签。他们编写了一种DSL,以指定这些条件,并用它以Ruby语言组装“语义模型”(第11章)。如果用Ruby将所有合同读入内存,再运行查询逻辑,那会非常慢,但是团队可以用语义模型的表示生成SQL,在数据库里做处理。直接用SQL编写规则,对开发人员都很困难,遑论业务人员。然而,业务人员可以读懂(在这种情况下,甚至编写)DSL里有关的表达式。
这样用DSL常常可以弥补宿主语言的局限性,将事物以适宜的DSL形式表现出来,然后,生成可用于实际执行环境的代码。
模型的存在有助于这种迁移。一旦有了一个模型,或者直接执行它,或者根据它产生代码都很容易。模型可以由表单风格的界面创建,也可以由DSL创建。DSL相对于表单有一些优势。在表述复杂逻辑方面,DSL比表单做得更好。而且,可以用相同的代码管理工具,比如版本控制系统,管理这些规则。当规则经由表单输入,存入数据库中,版本控制就无能为力了。
下面会谈及DSL的一个伪优点。我听说,有人宣称DSL的一个好处是,它能够在不同的语言环境下执行相同的行为。一个人编写了业务规则,然后生成C#或Java代码,或者,描述校验逻辑之后,在服务器端以C#形式运行,在客户端则是JavaScript。这是一个伪优势,因为仅仅使用模型就可以做到这一点,根本无需DSL。当然,DSL有助于理解这些规则,但那是另外一个问题。
2.2.4其他计算模型
几乎所有主流的编程语言都采用命令式的计算模型。这意味着,我们要告诉计算机做什么事情,按照怎样的顺序来做。通过条件和循环处理控制流,还要使用变量─确实,还有很多我们以为理所当然的东西。命令式计算模型之所以流行,是因为它们相对容易理解,也容易应用到许多问题上。然而,它并不总是最佳选择。
状态机是这方面的一个良好例子。可以采用命令式代码和条件处理这种行为,也确实可以很好地构建出这种行为。但如果直接把它当做“状态机”来思考,效果会更好。另外一个常见的例子是,定义软件构建方式。我们固然可以用命令式逻辑实现它,但后来,人们发现用“依赖网络”(第49章)(比如,运行测试必须依赖于最新的编译结果)解决会更容易。结果,人们设计出了专用于描述构建的语言(比如Make和Ant),其中将任务间的依赖关系作为主要的结构化机制。
你可能经常听到,人们把非命令式方式称为声明式编程。之所以叫做声明式,是因为这种风格让人定义做什么,而不是用一堆命令语句来描述怎么做。
采用其他计算模型,并不一定非要有DSL。其他编程模型的核心行为也源自“语义模型”(第11章),正如前面所讲的状态机。然而,DSL还是能够带来很大的转变,因为操作声明式程序,组装语义模型会容易一些。