第 10 章 基于Lambda的领域特定语言

本章内容

  • 领域特定语言(domain-specifc language, DSL)及其形式
  • 为你的API添加DSL都有哪些优缺点
  • 除了简单的基于Java的DSL之外,JVM还有哪些领域特定语言可供选择
  • 从现代Java接口和类中学习领域特定语言
  • 高效实现基于Java的DSL都有哪些模式和技巧
  • 常见Java库以及工具是如何使用这些模式的

开发者们经常忽略一点,编程语言首先是一门语言。任何语言的主要目标都是以最清晰、最容易理解的方式传递信息。也许一个优雅的软件的最重要特质就是能清楚明了地表达它的意图;或者,就像著名的计算机科学家Harold Abelson所说的那样:“我们编写程序首先是给人阅读的,机器只是偶尔执行一下。”

代码的易读性和易理解性在软件中的重要性甚至更胜一筹,因为软件需要对应用的核心业务逻辑进行建模。如果程序员书写的代码可以在开发团队与领域专家团队之间共享,并且很容易被双方理解的话,对提升团队的生产力将非常有利。这样一来,领域专家在软件开发的早期就可以介入开发流程,从业务需求的角度对软件的正确性进行验证。缺陷和误解可以在很早的阶段就被发现并解决。

为了达到这个目标,通常的做法是使用领域特定语言(DSL)来表达应用的业务逻辑。DSL是一种小型语言,它们大多都不通用,为某个特定领域客制化而生。DSL使用该领域特有的术语。如果你对Maven和Ant比较熟悉,就可以将它们看成描述构建过程的DSL。你可能还熟悉HTML,它是为描述网页结构而定制化的语言。历史上,Java由于它的僵硬与极度烦琐,很少被用于实现精简DSL,即使有人用它创建了DSL,也不便于非技术岗的人阅读和理解。不过,现在由于Java支持了Lambda表达式,你的工具箱中新增了一个强大的工具!实际上,我们在第3章就已经看到了Lambda表达式在降低代码复杂度方面的巨大威力,还记得它对示例程序“信号/噪声比”的改进吗?

设想一下如何用Java实现一个数据库。实现一个数据库时,程序员需要编写大量非常细节的底层代码,譬如选择哪一块磁盘存储指定的数据,为表建立索引,处理并发事务,等等。开发这样的一个数据库只有相当资深的程序员才能够胜任。假设你现在需要编写一个程序执行第4章和第5章中探讨过的那种查询,即“找出菜单中热量小于400卡路里的所有菜肴”。

持传统思维观点的专家程序员可能很快就给出了底层风格的实现代码,并认为这简直就是小菜一碟:

  1. while (block != null) {
  2. read(block, buffer)
  3. for (every record in buffer) {
  4. if (record.calorie < 400) {
  5. System.out.println (record.name);
  6. }
  7. }
  8. block = buffer.next();
  9. }

上面这个解决方案有两个主要问题:缺乏经验的程序员很难创建这样的程序(它需要程序员对诸如锁、I/O或者磁盘分配这样的细节有足够深入的理解),更重要的一点,这段代码的出发点是系统,而不是从应用角度出发。

一个用户导向型的程序员可能会问“为什么你不能给我提供一个SQL接口呢?这样我就可以编写SELECT name FROM menu WHERE calorie < 400这样的语句拿到需要的数据了。这里的menu是一张SQL的表,它保存了餐馆的菜单记录。采用这种方式我的开发效率会高很多,比由系统层出发的考虑快了不知道多少倍!谁关心那些系统底层的细节啊!”这种观点似乎也很难反驳!从本质上讲,这位程序员就是在要求使用DSL去操作数据,而不是用单纯的Java代码。严格来讲,这种类型的DSL应该叫“外部DSL”,因为它期望数据库提供一套API去解析和处理由纯文本编写的SQL语句。本章后续内容中会介绍更多外部DSL和内部DSL的区别。

但是如果回顾一下第4章和第5章的内容,你大概会意识到如果使用Stream API,那么这段代码可以更加精简,如下所示:

  1. menu.stream()
  2. .filter(d -> d.getCalories() < 400)
  3. .map(Dish::getName)
  4. .forEach(System.out::println)

这段代码中使用了链接(chaining)方法,这是Stream API的典型特征之一,也经常被称为流畅式(fluent style),相对于Java中循环复杂的控制流,这种方式理解起来容易多了。

这种格式高效地复制到了DSL中。这里提到的DSL,不是外部DSL,而是内部DSL。在内部DSL中,应用层原语被作为Java方法导出,用于操作代表数据库的一个或多个类的类型,与此相对的就是那些外部DSL中非Java格式的原语,譬如前面提到的SQL语句SELECT FROM。

从本质上讲,设计一个DSL包括决定哪些操作应该由应用程序员执行(还要特别小心,尽量避免暴露系统层的概念,以免引入不必要的污染),并为程序员提供这些对应的操作。

对于内部DSL,这个流程意味着需要暴露恰当的类和方法,以使代码的编写逻辑更流畅。创建外部DSL需要付出更大的代价,你不仅要设计DSL的语法,还需要为DSL实现解析器和执行器(evaluator)。不过这些都是值得的,如果你的设计没什么问题,那么初级程序员也可以快速且有效地完成代码的编写(这样你就有了源源不断的现金流,让你的公司能够在市场上屹立不败),而不需要直接改动你优美(不过对新手而言难于理解)的系统代码!

本章会通过几个例子和用例使你了解什么是DSL,什么时候你需要实现一个DSL,以及由此你能获得什么好处。接着会介绍几个Java 8 API中引入的小型DSL。你还将学习如何借助于同样的模式创建自己的DSL。最终,我们会一起分析目前市面上广为使用的几个Java库和框架,看看它们是如何应用这些技巧,凭借一系列的DSL提供的功能,让它们的API更易于使用的。