21

走近“领域特定语言”

作者: baiyuzhong 分类:图书推荐   阅读:18,990 次 添加评论

/Martin Fowler

领域特定语言(Domain-Specific Languages,简称DSL),是一个很有用的术语和概念,但其边界很模糊。一些东西很明显是DSL,但另一些可能会引发争议。本文接下来就走近“领域特定语言”,了解它的概念、为何要采用DSL,以及使用时应该注意的问题。

定义DSL

领域特定语言(名词),针对某一特定领域,具有受限表达性的一种计算机程序设计语言。这一定义包含4个关键元素:

计算机程序设计语言(computer programming language):人们用DSL指挥计算机去做一些事。同大多数现代程序设计语言一样,其结构设计成便于人们理解的样子,但它应该还是可以由计算机执行的语言。

  • 语言性(language nature):DSL是一种程序设计语言,因此它必须具备连贯的表达能力——不管是一个表达式还是多个表达式组合在一起。
  • 受限的表达性(limited expressiveness):通用程序设计语言提供广泛的能力:支持各种数据、控制,以及抽象结构。这些能力很有用,但也会让语言难于学习和使用。DSL只支持特定领域所需要特性的最小集。使用DSL,无法构建一个完整的系统,相反,却可以解决系统某一方面的问题。
  • 针对领域(domain focus):只有在一个明确的小领域下,这种能力有限的语言才会有用。这个领域才使得这种语言值得使用。

注意,“针对领域”在这个列表中最后出现,它纯粹是受限表达性的结果。很多人按字面意思把DSL理解为一种用于专用领域的语言。但字面意思常常有误:比如,我们不会管硬币叫“光盘”(Compact Disk,紧凑的盘),即便它确实是“盘”,而且相比于可以用这个术语称呼的东西,更为紧凑(compact)。

DSL主要分为三类:外部DSL、内部DSL,以及语言工作台。

  • 外部DSL是一种“不同于应用系统主要使用语言”的语言。外部DSL通常采用自定义语法,不过选择其他语言的语法也很常见(XML就是一个常见选择)。宿主应用的代码会采用文本解析技术对使用外部DSL编写的脚本进行解析。一些小语言的传统UNIX就符合这种风格。可能经常会遇到的外部DSL的例子包括:正则表达式、SQLAwk,以及像StrutsHibernate这样的系统所使用的XML配置文件。
  • 内部DSL是一种通用语言的特定用法。用内部DSL写成的脚本是一段合法的程序,但是它具有特定的风格,而且只用到了语言的一部分特性,用于处理整个系统一个小方面的问题。用这种DSL写出的程序有一种自定义语言的风格,与其所使用的宿主语言有所区别。这方面最经典的例子是LispLisp程序员写程序就是创建和使用DSLRuby社区也形成了显著的DSL文化:许多Ruby库都呈现出DSL的风格。特别是,Ruby最著名的框架Rails,经常被认为是一套DSL
  • 语言工作台是一个专用的IDE,用于定义和构建DSL。具体来说,语言工作台不仅用来确定DSL的语言结构,而且是人们编写DSL脚本的编辑环境。最终的脚本将编辑环境和语言本身紧密结合在一起。

多年来,这三种风格分别发展了自己的社区。你会发现,那些非常擅长使用内部DSL的人,完全不了解如何构造外部DSL。我担心这可能会导致人们不能采用最适合的工具来解决问题。我曾与一个团队讨论过,他们采用了非常巧妙的内部DSL处理技巧来支持自定义语法,但我相信,如果他们使用外部DSL的话,问题会变得简单许多。但由于对如何构造外部DSL一无所知,他们别无选择。因此,在本书中,把内部DSL和外部DSL讲清楚对我来说格外重要,这样你就可以了解这些信息,做出适当的选择。(语言工作台稍显粗略,因为它们很新,尚在演化之中。)

另一种看待DSL的方式是:把它看做一种处理抽象的方式。在软件开发中,我们经常会在不同的层面上建立抽象,并处理它们。建立抽象最常见的方式是实现一个程序库或框架。操纵框架最常见的方式是通过命令/查询式API调用。从这种角度来看,DSL就是这个程序库的前端,它提供了一种不同于命令/查询式API风格的操作方式。在这样的上下文中,程序库成了DSL的“语义模型”,因此,DSL经常伴随着程序库出现。事实上,我认为,对于构建良好的DSL 而言,语义模型是一个不可或缺的附属物。

谈及DSL,人们很容易觉得构造DSL很难。实际上,通常是难在构造模型上,DSL只是位于其上的一层而已。虽然让DSL 工作良好需要花费一定的精力,但相对于构建底层模型,这一部分的付出要少多了。

为何需要DSL

DSL只是一种工具,关注点有限,无法像面向对象编程或敏捷方法论那样,引发软件开发思考方式的深刻变革。相反,它是在特定条件下有专门用途的一种工具。一个普通的项目可能在多个地方采用了多种DSL——事实上很多项目这么做了。

DSL有其自身的价值。当考虑采用DSL时,要仔细衡量它的哪些价值适合于我们的情况。

1)提高开发效率

DSL的核心价值在于,它提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。拿格兰特小姐控制器的定义来说,相比于采用命令–查询APIDSL形式对我们而言更容易理解。

这种清晰并非只是审美追求。一段代码越容易看懂,就越容易发现错误,也就越容易对系统进行修改。因此,我们鼓励变量名要有意义,文档要写清楚,代码结构要写清晰。基于同样的理由,我们应该也鼓励采用DSL

人们经常低估缺陷对生产率的影响。缺陷不仅损害软件的外部质量,还浪费开发人员的时间去调查以及修复,降低开发效率,并使系统的行为异常,播下混乱的种子。DSL的受限表达性,使其难于犯错,纵然犯错,也易于发现。

模型本身可以极大地提升生产率。通过把公共代码放在一起,它可以避免重复。首先,它提供了一种“用于思考问题”的抽象,这样,更容易用一种可理解的方式指定系统行为。DSL提供了一种“对阅读和操作抽象”更具表达性的形式,从而增强了这种抽象。DSL还可以帮助人们更好地学习使用API,因为它将人们的关注点转移到怎样将API方法整合在一起。

我还遇到过一个有趣的例子,使用DSL封装一个棘手的第三方程序库。当命令–查询接口设计得很糟糕时,DSL 惯常的连贯性就得以凸现。此外,DSL只须支持客户真正用到的部分,这大大降低了客户开发人员学习的成本。

2)与领域专家的沟通

我相信,软件项目中最困难的部分,也是项目失败最常见的原因,就是开发团队与客户以及软件用户之间的沟通。DSL提供了一种清晰而准确的语言,可以有效地改善这种沟通。

相比于关于生产率的简单争论,改善沟通所带来的好处显得更加微妙。首先,很多DSL并不适用于沟通领域问题,比如,用于正则表达式或构建依赖关系的DSL,在这些情况下就不合适。只有一部分独立DSL确实应用这种沟通手段。

当在这样的场景下讨论DSL时,经常会有人说:“好吧,现在我们不需要程序员了,领域专家可以自己指定业务规则。”我把这种论调称为“COBOL谬论”——因为COBOL曾被人寄予这样的厚望。这种争论很常见,不过,我觉得这种争论不值得在此重复。

虽然存在“COBOL谬论”,我仍然觉得DSL可以改善沟通。不是让领域专家自己去写DSL,但他们可以读懂,从而理解系统做了什么。能够阅读DSL代码,领域专家就可以指出问题所在。他们还可以同编写业务规则的程序员更好地交流,也许,他们还可以编写一些草稿,程序员们可以将其细化成适当的DSL规则。

但我不是说领域专家永远不能编写DSL。我遇见过很多团队,他们成功地让领域专家用DSL编写了大量系统功能。但我仍然认为,使用DSL的最大价值在于,领域专家能够读懂。所以编写DSL的第一步,应该专注于易读性,这样即便后续的目标达不到,我们也不会失去什么。

使用DSL是为了让领域专家能够看懂,这就引出了一个值得争议的问题。如果希望领域专家理解一个“语义模型”的内容,可以将模型可视化。这时就要考虑一下,相比于支持一种DSL,是不是只使用可视化会是一种更有效的办法。可视化对于DSL而言,是一种有益的补充。

让领域专家参与构建DSL,与让他们参与构建模型是同样的道理。我发现,与领域专家一起构建模型能够带来很大的好处,在构建一种Ubiquitous Language [Evans DDD] 的过程中,程序员与领域专家之间可以深入沟通。DSL提供了另一种增进沟通的手段。随着项目的不同,我们可能发现,领域专家可能会参与模型和DSL,也可能只参与 DSL

实际上,有些人发现,即便不实现DSL,有一种描述领域知识的DSL,也能带来很大的好处。即使只把它当做沟通平台也可以获益。

总的来说,让领域专家参与构建DSL比较难,但回报很高。即使最终不能让领域专家参与,但是开发人员在生产率方面的提升,也足以让我们大受裨益,因此,DSL值得投入。

3)执行环境的改变

当谈及将状态机表述为XML的理由时,一个重要的原因是,状态机定义可以在运行时解析,而非编译时。在这种情况下,我们希望将代码运行于不同的环境,这类理由也是使用DSL一个常见的驱动力。对于XML配置文件而言,将逻辑从编译时移到运行时就是一个这样的理由。

还有一些需要迁移执行环境的情况。我曾见过一个项目,它要从数据库里找出所有满足某种条件的合同,给它们打上标签。他们编写了一种DSL,以指定这些条件,并用它以Ruby语言组装“语义模型”。如果用Ruby将所有合同读入内存,再运行查询逻辑,那会非常慢,但是团队可以用语义模型的表示生成SQL,在数据库里做处理。直接用SQL编写规则,对开发人员都很困难,遑论业务人员。然而,业务人员可以读懂(在这种情况下,甚至编写)DSL里有关的表达式。

这样用DSL常常可以弥补宿主语言的局限性,将事物以适宜的DSL形式表现出来,然后,生成可用于实际执行环境的代码。

模型的存在有助于这种迁移。一旦有了一个模型,或者直接执行它,或者根据它产生代码都很容易。模型可以由表单风格的界面创建,也可以由DSL创建。DSL相对于表单有一些优势。在表述复杂逻辑方面,DSL比表单做得更好。而且,可以用相同的代码管理工具,比如版本控制系统,管理这些规则。当规则经由表单输入,存入数据库中,版本控制就无能为力了。

下面会谈及DSL的一个伪优点。我听说,有人宣称DSL的一个好处是,它能够在不同的语言环境下执行相同的行为。一个人编写了业务规则,然后生成C#Java代码,或者,描述校验逻辑之后,在服务器端以C#形式运行,在客户端则是JavaScript。这是一个伪优势,因为仅仅使用模型就可以做到这一点,根本无需DSL。当然,DSL有助于理解这些规则,但那是另外一个问题。

4)其他计算模型

几乎所有主流的编程语言都采用命令式的计算模型。这意味着,我们要告诉计算机做什么事情,按照怎样的顺序来做。通过条件和循环处理控制流,还要使用变量——确实,还有很多我们以为理所当然的东西。命令式计算模型之所以流行,是因为它们相对容易理解,也容易应用到许多问题上。然而,它并不总是最佳选择。

状态机是这方面的一个良好例子。可以采用命令式代码和条件处理这种行为,也确实可以很好地构建出这种行为。但如果直接把它当做“状态机”来思考,效果会更好。另外一个常见的例子是,定义软件构建方式。我们固然可以用命令式逻辑实现它,但后来,人们发现用“依赖网络”(比如,运行测试必须依赖于最新的编译结果)解决会更容易。结果,人们设计出了专用于描述构建的语言(比如MakeAnt),其中将任务间的依赖关系作为主要的结构化机制。

你可能经常听到,人们把非命令式方式称为声明式编程。之所以叫做声明式,是因为这种风格让人定义做什么,而不是用一堆命令语句来描述怎么做。

采用其他计算模型,并不一定非要有DSL。其他编程模型的核心行为也源自“语义模型”,正如前面所讲的状态机。然而,DSL还是能够带来很大的转变,因为操作声明式程序,组装语义模型会容易一些。

DSL的问题

前面已经讨论了何时该采用DSL,接下来就该谈论什么时候不该采用DSL,或者至少是使用DSL应注意的问题。

从根本上说,不使用DSL的唯一原因就是,在你的场景下,使用DSL得不到任何好处,或者,至少是DSL的好处不足以抵消构建它的成本。

虽然DSL在有些场合下适用,但同样会带来一些问题。总的来说,我认为通常是高估了这些问题,一般人们不太熟悉如何构造DSL,以及DSL如何适应更为广阔的软件开发图景。还有,许多常提及的DSL问题混淆了DSL和模型,这也伤及了DSL的优势。

许多DSL问题只是与某种特定DSL风格相关,要理解这些问题,我们需要深入理解这些DSL是如何实现的。所以,这些问题留待后面讨论,在这里,我们只看宽泛的问题,这同当前讨论的问题是一致的。

1)语言噪音

在反对DSL的观点中,我最常听到的是称为语言噪音的问题:担心语言难于学习,因此,使用多种语言会比使用一种语言复杂得多。必须了解多种语言,会让工作更为困难,新人加入的门槛也提升了。

当人们谈及这种担心时,他们都会有一些共同的误解。首先,他们通常混淆了学习一门DSL的心血与学习一门通用语言的心血。DSL远比一门通用语言容易,因此,学习起来也要容易得多。

许多批评者知道这一点,但依然反对DSL,即便它们相对容易学习,在一个项目上有多种DSL也增加了理解的难度。这里的误解在于,他们忘了一点,一个项目总有一些复杂的地方,难于学习。即便不用DSL,代码库中仍然有许多需要理解的抽象。通常,这些抽象应该在程序库里,以便于掌握。即使不必学习多种DSL,也不得不学习多个程序库。

所以,真正的问题在于,相比于学习DSL底层模型而言,学习DSL会难多少。我认为,相对于理解模型而言,学习DSL 所增加的成本相当小。确实,因为DSL的价值就在于,让人们理解和使用模型更容易,所以使用DSL就应该能降低学习成本。

2)构建成本

相对于底层的程序库而言,DSL增加的成本并不大,但这始终是成本。代码需要写,尤其是还要维护。所以,同其他代码一样,它也要做好自己的本职工作。并非所有程序库都值得用DSL封装。如果命令–查询API够用,就没有必要在上面提供额外的API。即便DSL有用,就边界效应而言,构建和维护也需要花费太多的工作量。

DSL的可维护性是一项重要的考量因素。如果团队中的大多数人都觉得难以理解,即使是一种简单的内部DSL,也会带来很大的麻烦。外部DSL更是让许多人望而却步,一个解释器就足以让很多程序员打退堂鼓。

人们不习惯构建DSL,这也让添加DSL的成本变得更高。人们要学习新技术。虽然不应该忽略这些成本,但我们也应该清楚,这个学习曲线的成本能够分摊到未来使用DSL的过程中。

还有一点要清楚,DSL的成本大于构建模型的成本。任何复杂的地方都需要某种机制管理其复杂性,如果复杂到要考虑DSL,几乎肯定复杂到可以从模型中获益的程度。DSL有助于思考模型,降低构建成本。

这会带来一个相关问题,鼓励使用DSL会导致构建出一堆糟糕的DSL。实际上,我盼着构建出一堆糟糕的DSL,就像有很多糟糕的命令–查询API的程序库一样。问题在于,DSL会不会把事情弄得更糟。一个好的DSL可以封装一个糟糕的程序库,把它变得更易用(如果可能的话,我更愿意修正程序库本身)。糟糕的DSL对于构建和维护而言,就是浪费资源,但这种说法对任何代码都适用。

3)集中营语言

集中营语言(ghetto language)问题与语言噪音问题正好相反。比如,一家公司用一种内部语言编写公司内的很多系统,这种语言在其他地方根本用不上。这种做法会让他们很难找到新人,跟上技术变化。

在分析这个问题时,首先要澄清一点,如果整个系统都是用一种语言编写的,那它就不是一种DSL(至少按我的定义),而是一种通用语言。虽然可以用许多DSL技术构建通用语言,但我强烈建议,不要这样做。构建和维护一种通用语言是一个巨大的负担,它会迫使你在这个集中营中做大量工作,甚至挣扎一生。不要这么做。

我相信,集中营语言问题并非空穴来风,它隐含了一些现实问题。首先是,一种DSL总是存在着无意中演化成一种通用语言的危险。我们有一种DSL,然后,逐步为它添加新功能;今天添加条件表达式,明天又添加循环,最终图灵完备了。

对此,唯一的抵御就是坚决防范。确保我们对DSL针对问题的受限范围有个清晰的认识。质疑任何不在此范畴内的新特性。如果想做得更多,可以考虑采用多种语言,综合运用,而非强求一种DSL不断膨胀。

框架也面临着同样的问题。好的程序库都有一个明确的目的。如果产品定价库包含HTTP协议的实现,从本质上说,我们也就要忍受同样错误之苦:未能分离关注点。

第二个问题是,自行构造本应从外部获得的东西。这个问题同样适用于DSL和程序库。比如,如今,很少有要自己构造对象–关系映射(objectrelational mapping)系统。我有一条关于软件的通用规则,不是自己的业务,不要自己写——总要先看看是否从别的地方可以找到。特别是,随着开源工具的崛起,基于既有开源工作量进行扩展,肯定比从头打造更有意义。

4)“一叶障目”的抽象

DSL的有用之处在于,它提供了一种抽象,我们可以基于这种抽象来思考领域问题。这种抽象非常有价值,我们更容易表述领域行为,效果远胜于依据底层构造进行思考。

然而,任何抽象(包括DSL和程序库)总是伴随着风险——它可能让我们“一叶障目,不见泰山”。有了这种“一叶障目”的抽象,我们就会苦苦思索,竭尽全力把外部世界塞入抽象之中,而非另寻它路。我们常常会见到这种情况:遇到一种不符合抽象的事物,殚精竭虑地让其符合,而不是修改抽象,让抽象更容易接纳新的行为。一旦我们满意了这个抽象,觉得尘埃落定,“一叶障目”也就随之而来。到这种时候,对于颠覆性的变化,难免心生忧虑。

“一叶障目”是任何抽象都会面临的问题,不仅是DSL,但DSL可能让这个问题变得更严重。因为DSL提供了一种更为舒适的方式操作抽象,一旦适应,更不愿意做出改变。如果采用DSL与领域专家交流,问题可能会更严重,通常,他们在习惯之后更不愿意做出改变。

如同对待任何抽象一样,应该视DSL为一种“不断演化,尚未完结”的事物。

作者Martin Fowler,世界级软件开发大师,软件开发教父,敏捷开发方法的创始人之一,在面向对象分析与设计、UML、模式、极限编程、重构和DSL等领域都有非常深入的研究并为软件开发行业做出了卓越贡献。他乐于分享,撰写了《企业应用架构模式》(荣获第13Jolt生产力大奖)、《重构:改善既有代码的设计》、《分析模式:可复用的对象模型》、《UML精粹:标准对象建模语言简明指南》等在软件开发领域颇负盛名的著作。

本文节选自《领域特定语言》一书。Martin Fowler著,ThoughtWorks中国译,由机械工业出版社出版。

 

转播到腾讯微博

----->立刻申请加入《程序员》杂志读者俱乐部,与杂志编辑直接交流,参与选题,优先投稿

请评论

preload preload preload
京ICP备06065162