返回介绍

2.2 DSL(特定领域语言)

发布于 2023-05-19 13:36:37 字数 14457 浏览 0 评论 0 收藏 0

下面,我们来介绍一些值得关注的编程语言功能。首先我们从 DSL(Domain Specific Language,特定领域语言)开始说起。

所谓 DSL,是指利用为特定领域(Domain)所专门设计的词汇和语法,简化程序设计过程,提高生产效率的技术,同时也让非编程领域专家的人直接描述逻辑成为可能。DSL 的优点是,可以直接使用其对象领域中的概念,集中描述“想要做到什么”(What)的部分,而不必对“如何做到”(How)进行描述。

例如,有一个名为 rake 的编译工具,它是用 Ruby 编写的。这个工具的功能跟 make 差不多,它会分析文件的依赖关系,并自动执行程序的编译、连接等操作。其中描述依赖关系的 Rakefile 就是使用了 Ruby 语法的一种 DSL。图 1 就是 Rakefile 的一个简单的例子。

task :default => [:test]
task :test do
  ruby "test/unittest.rb"
end
图 1 Rakefile 示例

这个例子表达了下面两个意思:

1. 默认任务为 test

2. test 的内容是执行 test/unittest.rb

启动 rake 命令的格式是:

rake 任务
如果这里的任务省略的话,则执行 default 任务。

在 Rakefile 对于依赖关系的描述中只是指定了 task,而对于内部数据结构是怎样的,以及维持依赖关系要如何实现等等具体问题都无需涉及,因为具体的实现方式,与描述依赖关系这个对象领域(Domain)是无关的。

DSL 这个对特定目的小规模语言的称呼,是最近才出现的比较新的叫法,但这种方法本身却并不是什么稀罕的东西,尤其是 UNIX 社区中就诞生了许多用来解释像这样的“专用语言”的工具。

其中,以行为单位进行文本处理的脚本语言 awk 算是比较有名的,除此之外,UNIX 中还开发了很多“迷你语言”,比如用来描述依赖关系的 Makefile 和用来读取它的 make 命令、以行为单位进行数据流编辑的 sed 命令、用来描述文档中嵌入图像的 pic 命令,用来生成表格的 tbl 命令等等。此外,为了对这些迷你语言的编写提供支持,UNIX 中还提供了语法分析器生成工具 yacc,以及词法分析器生成工具 lex,而 yacc 和 lex 本身也拥有自己的迷你语言。

这些迷你语言,几乎都是专用于特定用途的,大多数情况下无法完成复杂的工作,但它们都能够比简单的配置文件描述更多的内容,并为命令的处理带来了更大的灵活性,因此和 DSL 在本质上是相同的。

外部 DSL

像以这些迷你语言为代表的,由专用的语言引擎来实现的 DSL,称为外部 DSL。UNIX 的迷你语言文化是先于 DSL 出现的,但并非只有 UNIX 才提供外部 DSL。

在 Java 编写的应用程序中,大量使用了由可扩展标记语言(XML)编写的配置文件。这种 XML 配置文件也是外部 DSL 的一种形式。此外,数据库访问所使用的 SQL(Structured Query Language,结构化查询语言)也是一种典型的外部 DSL。

外部 DSL 的优点,在于它是独立于程序开发语言的。对于某个领域进行操作的程序,不一定是用同一种语言来编写的。例如,用来对数据库进行操作的程序,有时可以用 Ruby 来开发,有时也可以用 PHP 或者 Java 来开发。由于外部 DSL 是独立于程序开发语言的,因此可以实现跨语言共享。只要学会了 SQL,就可以在不同的语言中,用相同的 SQL 来进行数据库操作。

正则表达式的使用方法也差不多。正则表达式是用来描述字符串模板的一种外部 DSL,在 Ruby、Perl、PHP、Python 等很多语言中都可以使用,其语法在不同的语言中基本上都是通用的1。这样的好处是,在不同的语言中都可以使用字符串模板匹配这一通用功能。

1 实际上在不同引擎中,正则表达式的功能和语法也有细微差别。(原书注)

此外,外部 DSL 实际上是全新设计的语言和语言引擎,因此可以根据它的目的进行自由的设计,不必被特定的执行模块和现有语言的语法所左右。由于自由度很高,在特定领域中能够大幅度提高生产效率。

也许大家认为设计和实现一种语言是非常辛苦的工作,但如果规模不是很大的话,实际上也没有那么难。以名著《UNIX 编程环境》2为首的许多书籍中,都以迷你语言的制作作为例题,其核心部分只要几页的代码就可以完成。

2 《UNIX 编程环境》(The Unix Programming Environment),Brain W. Kernighan 和 Rob Pike 著,1984 年出版,两位作者来自贝尔实验室,这本书是关于 Unix 操作系统的早期著名著作。中译本由机械工业出版社于 1999 年发行,陈向群等译。

然而,高度自由的设计也是一把双刃剑,这意味着程序员为了使用 DSL 必须学习一门全新的语言。像 SQL 这样作为数据库访问的通用方式已经实现普及和标准化的语言还好,如果为了每一种目的都要从零开始学习一门完全不同的语言,这样的辛苦可不是闹着玩的。

内部 DSL

和外部 DSL 相对的自然就是内部 DSL 了。外部 DSL 是从 UNIX 中脱胎发展而来的,而内部 DSL 则是发源于 Lisp 和 Smalltalk 的文化之中。

内部 DSL 并不是创造一种新的语言,而是在现有语言中实现 DSL,而作为 DSL 基础的这种现有语言,称为宿主语言。从原理上说,宿主语言可以是任何编程语言,不过还是有适合不适合之分。Lisp、Smalltalk、Ruby 这些语言适合作为 DSL 的宿主语言,因此在这些语言的社区中经常使用内部 DSL。

内部 DSL 的优点和缺点和外部 DSL 正好相反。也就是说,内部 DSL 是“借宿”在宿主语言中的,它借用了宿主语言的语法,因此程序员无需学习一种新的语言。在理解内部 DSL 含义时,宿主语言的常识依然有效,而学习新语言时,宿主语言的知识也会成为不错的航标。

之前我们举过 rake 命令的 Rakefile 这个例子,这就是一种内部 DSL。图 1 中显示的 Rakefile 代码,是用来为 rake 命令描述编译规则的 DSL 代码,但同时它也是一段 Ruby 程序。

内部 DSL 还有其他一些优点。当用 DSL 编写复杂的逻辑时,可以使用过程定义、条件分支、循环等作为通用语言的宿主语言所具备的全部功能。从某种意义上说,它就是万能的。

此外,“寄生”在宿主语言上面,就意味着 DSL 的实现会相对容易。一种迷你语言的实现再怎么简单,和在宿主语言基础上增加一些功能就能够实现的内部 DSL 相比,都只能甘拜下风了。

说到内部 DSL 的缺点,由于 DSL 的语法被限定在宿主语言能够描述的范围内,因此自由度比较低。不过,自由度低换来了较好的易读性,何况像 Lisp 这样具备高性能宏功能的语言中,即便是内部 DSL(以一定的易读性为代价)也可以实现相当高的自由度。

我的个人观点是,从易读性和实现的容易性来看,内部 DSL 具备更多的优势。

DSL 的优势

那么 DSL 有哪些优势呢?为什么 DSL 近年来如此备受关注呢?

这是因为 DSL 在几个方面上可以说掌握了提高生产效率的关键。DSL 拥有为特定领域所设计的词汇,可以在高级层面上编写程序。由于不需要编写多余的部分,因此就节约了程序开发的时间。

此外,使用 DSL 可以让程序在整体上以更简洁的形式进行表达,这意味着无论是写程序还是读程序的成本都降低了,同时也意味着对于非编程专家的一般人来说,编程的大门正向他们敞开。

很多人觉得编程很难,但如果自己的专业领域中有适用的 DSL 的话,情况就不同了。如果可以将想让计算机完成的任务直接描述出来,也许就可以减少与程序员交流的成本,从而实现生产效率的提高。这才是 DSL 备受关注的一个最大的理由。

仔细想想就会发现,不涉及对象领域的内部细节,而是在高级层面上进行描述,这就是近半个世纪以来编程语言进化的方向——抽象化。也就是说,DSL,尤其是内部 DSL,也许就是由抽象化的不断推进所引领的编程语言未来发展的方向之一吧。

DSL 的定义

谈到 DSL,大家总是热衷于讨论到底怎样算是 DSL。关于什么是 DSL,什么不是 DSL,并没有明确的定义和标准。

有一种观点认为,像“是否具备仅用于特定用途的功能”、“(设计者)自己是否将其命名为一种 DSL”等可以作为判断的标准,但实际上这些标准也是非常模棱两可的。

尽管如此,考虑到 DSL 实际上是编程语言抽象化的延伸,那么问题就不应该是什么是 DSL、什么不是 DSL,DSL 应该是将面向特定领域的 API 设计成优秀的 DSL 这样一个设计的过程。

据说,在诞生了 UNIX 的 AT&T 贝尔实验室3中有一句名言:库设计就是语言设计(Library design is language design)。我们在思考编程语言的时候,大多仅强调语法,但如果脱离了相当于词汇的库、类和方法,语言也就无从思考。

3 贝尔实验室(Bell Laboratories)是一个以电信相关研究起家的研究开发机构,由 AT&T 和 Western Electric 共同出资创立于 1925 年,于 1996 年脱离 AT&T,目前属于阿尔卡特朗讯(Alcatel-Lucent)公司旗下。贝尔实验室在计算机领域的重大发明包括 UNIX 操作系统和 C 语言等,其他领域的重大发明包括晶体管、激光、CCD 等,许多成果都获得过诺贝尔奖。

也就是说,API(Application Programming Interface,应用程序编程接口)也是构成编程语言的一个重要要素。向一种语言添加库,也就相当于在设计一种“新增了一些词汇的规模更大一点儿的语言”。我们可以通过“编程达人”大卫·托马斯4的这句话理解这一过程:

4 大卫 • 托马斯(Dave Thomas)是一位程序员、作家、出版家,著有 Programming Ruby 等书。

Programming is a process of designing DSL for your own application.

(编程就是为自己的应用程序设计 DSL 的过程)

应用程序开发,可以理解为是将库等组件设计成针对该应用程序对象领域的 DSL,最后再进行整合的过程。这样编写出来的应用程序,其代码的抽象度高,应对未来修改的能力强,一定是一个不错的应用程序。

因此,DSL 并不仅仅是一种技术,而是应用程序开发的重要设计原理和原则之一,可以说适用于任何软件的开发。在设计 API 时,如果能像“设计一种新的 DSL”一样进行设计的话,感触应该会变得不同吧。

适合内部 DSL 的语言

正如刚才所说的,在 UNIX 文化中,由若干单一目的的小工具所组成的“工具箱系统”是主流,Linux 也继承了这一点。UNIX 中的各种迷你语言,作为组成工具箱的零部件,同时也作为为特定目的专用的外部 DSL,不断发展壮大起来。

不过,在现代 UNIX 文化也受到了很多来自外部的影响。例如,现在典型的 UNIX 文本编辑器 Emacs,其起源与其说是来自 UNIX,不如说是来自美国麻省理工学院(MIT)的 Lisp 文化。它的开发者理查德·斯托曼5本来就是一位 MIT 出身的 Lisp 黑客,因此这也是理所当然的吧。此外,从 Perl 到 Ruby 的这些脚本语言中,并不是采用小工具组合起来的方式,而是提供多功能可编程的工具,从这一点上看,也是受到了 Lisp 文化的影响。近年来内部 DSL 的备受关注,和这个倾向也不能说没有关联。

5 理查德 • 斯托曼(Richard Stallman,1953— )是一位著名的黑客,自由软件运动领导者,GNU 计划和自由软件基金会创始人,他所编写的 GNU 通用公共许可证(GNU GPL)是世界上最广为采用的自由软件许可证。

那么,什么样的语言适合用作内部 DSL 的宿主语言呢?虽然任何语言都可以成为宿主语言,但像 Lisp、Smalltalk、Ruby 这样被认为适合 DSL 的语言,都拥有一些共同的特征。

首先是简洁。由于 DSL 本来就是为了将针对特定目的处理用高级的、简洁的方式进行描述,因此简洁的描述方式才是最本质的。从这个意义上来讲,语言简洁是作为 DSL 宿主语言不可或缺的要素。Lisp 和 Ruby 等语言中,无需在程序中声明数据类型,编译器的“规矩”也比较少,因此能够让程序变得简洁。

作为宿主语言的另一个重要特征就是灵活性。在 DSL 中,开发者会通过高度抽象化的代码来集中描述 What,而不是 How,因此作为对抽象化的支持,元编程等功能也最好比较充实。

此外,Lisp 具备宏(Macro)功能,只要遵循“用括号进行表达”的 S 表达式6语法,就可以实现相当自由的表达。因此可以说 Lisp 语言本身就是一种可被编程的语言。

6 S 表达式(S-expression)是符号化表达式(Symbolic Expression)的简称,是 Lisp 中用于表达嵌套(层次状)数据的方式。

另一方面,Ruby 中的代码块(Block)功能可以实现控制结构。代码块虽然不像 Lisp 的宏那样万能,但用来实现内部 DSL 还是足够了。

让我们再回头看看图 1 中 Rakefile 的示例。在 Rakefile 中定义了一个名为 task 的方法,任务的名称则作为参数通过符号(Symbol)来指定。当这里定义的 test 任务被执行时,代码块就会被求值。像这样,在 Ruby 中通过使用代码块,就可以表达一种控制结构。

Rakefile 的代码之所以看上去很简洁,是因为 Ruby 的语法就是以这样的宗旨进行设计的。相比语法的单纯性来说,Ruby 更加重视程序的可读性,其语法也是以此为先决条件而确定的。

例如,调用方法时参数周围的括号是可以省略的,还可以通过代码块将整个一块代码作为参数传递给一个方法。如果说 Ruby 的语法追求的是没有任何冗余性的“简单”的话,那么图 1 的 Rakefile 代码就会变成图 2 这个样子。这样的语法虽说非常简单,但绝对算不上是易读和易写的。

task(:default => [:test])
task(:test, &lambda(){
  ruby("test/unittest.rb")
})
图 2 Rakefile 示例

那么,如果是一种不具备 Lisp 和 Ruby 这样简洁性和灵活性的语言,例如 Java,是不是就不可能用作内部 DSL 呢?的确,像 Java 这样的语言,由于必须要指定数据类型,代码容易变得非常繁杂,而且语法的自由度也不高,要实现像 Lisp 和 Ruby 一样的内部 DSL 是非常困难的。然而,以代码重构而闻名的马丁·福勒7则提出,通过“流畅接口”(Fluent interface)的方式,像 Java 这样的语言是不是也能够实现内部 DSL 一样的功能呢?图 3 就是他所展示的流畅接口的示例。

7 马丁 • 福勒(Martin Fowler,1963— )是一位软件开发方面的著名作家,这里所说的“代码重构”是指他在 1999 年与肯特 • 贝克等人合作编写的《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code)一书。

private void makeOrder(Customer
customer) {
    customer.newOrder()
            .with(6, "TAL")
            .with(5, "HPK").skippable()
            .with(3, "LGV")
            .priorityRush();
}
图 3 用 Java 编写的流畅接口(fluent interface)

图 3 中的代码定义了一张顾客(Customer)的订单。在订单中,包含了商品及其数量的订购明细。某些情况下,为了避免整张订单延迟发货,可以从订单中去掉某些货品,先将剩下有货的货品发出来,因此在这里的明细中,有些货品被定义为“赶不上货期的话可以跳过”。整张订单的状态被设定为“加急”。图 3 整个代码的含义是定义了如下这样一张订单:

· TAL,6 个

· HPK,5 个(可跳过)

· LGV,3 个

· 加急

流畅接口中运用了方法链(Method chain),作为 Java 来说这种表达方式是前所未有的简洁。如果用以前“更像 Java 的风格”来表达的话,就会变成图 4 这样。

private void makeOrder(Customer customer) {
    Order o1 = new Order();
    customer.addOrder(o1);
    OrderLine line1 = new OrderLine(6, Product.
find("TAL"));
    o1.addLine(line1);
    OrderLine line2 = new OrderLine(5, Product.
find("HPK"));
    o1.addLine(line2);
    OrderLine line3 = new OrderLine(3, Product.
find("LGV"));
    o1.addLine(line3);
    line2.setSkippable(true);
    o1.setRush(true);
}
图 4 Java 标准风格的接口

简洁性和易读性的区别一目了然。虽然流畅接口在 Java 社区还没有普及,不过这种设计思路在今后是非常值得期待的。

外部 DSL 实例

内部 DSL 代表了编程语言进化的一种形态,作为编程爱好者,我自然对其兴趣颇深,但在这里,我还想再谈谈外部 DSL。

刚才已经讲过,外部 DSL 就是拥有专用引擎的一种独立的特定领域语言,不过外部 DSL 也有各自不同的实现水平。

最简单的一种莫过于配置文件和数据文件了吧。例如 YAML、JSON 等语言,就是为了“将对象(用对人类易读的形式)描述出来”这一特定目的而设计的外部 DSL(图 5)。

[
  {
    "name": "松本行弘",
    "company": "NaCl",
    "zipcode": "690-0826",
    "address": "松江市学园南2-12-5",
    "tel": "0852-28-XXXX"
  }
]
---
图 5 外部 DSL JSON 中对数据的表达方式

而更复杂的一些 DSL,虽然也是为特定目的而设计,但却可以编写出描述任意算法的程序。例如用于文本处理的 awk 等,就属于这种水平。awk 程序的基本结构是从文件中以行为单位读取字符串,同时它具备每读取一行之后将字符串分割成字段等等这样的文本处理专用功能。这些功能明显是属于 DSL,但另一方面,它也并不是不能编写出文本处理范围以外的程序。再举一个例子,用于向打印机描述页面的 DSL PostScript,也是一种基于逆波兰记法8的图灵完全(拥有完整计算能力的)语言。

8 逆波兰记法(Reverse Polish notation)是一个将操作符置于操作数后面的“后缀式记法”,如“3 + 4”用这种记法要写成“3 4 +”。这种记法的好处是不必使用括号来表示运算顺序,并且对于计算机来说更容易实现。

另外还有一种比较特殊的,虽然它本身是一种通用语言,叫做 DSL 并不十分合适,但这种语言却与特定计算模型之间拥有很强的关联性,也就具备了很强的类似 DSL 的性质。例如 Prolog,它是一种以一阶谓词逻辑为基础的语言。Prolog 可以被认为是面向谓词逻辑这一特定领域的 DSL,但将谓词逻辑这个适用范围称作“特定领域”似乎未免太宽泛了,因此一般情况下人们也不会将 Prolog 称作 DSL。同样地,与用于并行计算的 Actor 模型9密切相关的 Erlang 等语言,虽然是一种通用语言,但它同时也具备“面向并发编程领域的 DSL”这一性质。

9 Actor 模型(Actor model)是一种用于并行计算的模型,其中每个 Actor 是并行计算的基本单元,它可以和其他 Actor 进行消息通信,也可以创建更多的 Actor。

让我觉得比较有意思的是 Java 中所使用的 XML。由于 Java 中默认内置了用于解析 XML 的库,因此如果用 XML 来编写 DSL 的话,就可以很容易地被程序读取。这样一来,基本上就可以省却为 DSL 开发语言引擎的步骤了。

通过这样的方式,我们可以用 XML 这一通用的、不被特定目的限制的语法,很容易地创造出新的外部 DSL,我认为这是一种非常高效的方式。只不过,XML 文件的内容是通过标签来描述的,看起来十分冗长,无论是阅读还是编写,对用户来说都不是很友好,这一点算是一个遗憾吧。

DSL 设计的构成要素

曾经在诸多 Ruby 相关活动中发表过演讲的著名 Rubyist——Glenn Vanderburg 认为,构成一种优秀的(内部)DSL 的要素包括下列 5 种:

· 上下文(Context)

· 语句(Sentence)

· 单位(Unit)

· 词汇(Vocabulary)

· 层次结构(Hierarchy)

其中的上下文,用来针对 DSL 中每个单独的语句,规定其所拥有的含义。也许有人认为:“用参数的方式进行显式指定不就好了吗?”不过大家别忘了,DSL 的宗旨是进行简洁的描述,如果每次都通过参数来反复指定上下文的话,程序必定会变得冗长。

请看图 6 中的程序。这是用描述测试 用的库 shoulda 来编写的测试程序,它也是一种以 Ruby 为基础的内部 DSL。

class UserTest < Test::Unit::TestCase
  context "a User instance" do
    setup do
      @user = User.find(:first)
    end

    should "return its full name" do
      assert_equal 'John Doe', @user.full_
name
    end
  end
end
图 6 shoulda 编写的测试程序

在图 6 的例子中,context 方法和 should 方法就定义了上下文。顾名思义,context 方法的作用就是定义上下文,它表示这个测试项目的一个大的框架,而上下 文的范围是通过指派给 context 方法的代码块来定义的。因此:

context "a User instance" do
就表示“对 a User instance 这个上下文的测试进行定义”的意思。

should 方法则定义了“需要满足某个条件”这样一个测试。当指派给 should 方法的代码块所描述的测试成功时,则视为满足该测试的条件。should 方法所定义的测试,和外部的上下文结合起来,就定义了“a User instance should return its full name”(用户实例应当返回其全名)这一软件的行为。

这样的方式与其说是测试,不如说是定义了软件所应该具有的行为(Behavior),因此更多的情况下人们不会将其称为测试,而是称为规格(Spec)。此外,在软件开发之前就设计好规格的开发手法,通常不是叫做测试驱动开发,而是叫做行为驱动开发(Behavior Driven Development,BDD)。

说完了上下文,下一个 DSL 的构成要素是“语句”。语句也就是上下文中每条独立的代码,在内部 DSL 中实现函数和方法的调用。

貌似英语圈的人总是把让语句尽可能看起来接近英语这一点看得很重要。DSL 的优点之一,就是让并非专家的普通人也能够使用,因此,为不太懂编程的人降低点门槛,这样所带来的好处也是可以理解的。不过,作为我来说(也是因为我英语不是很好的缘故吧),我觉得看上去像英语并不是 DSL 的本质,不过还是有很多人执着于这一点。

在几年前的一次 Ruby 大会中有一个以 DSL 为主题的段落,讲的内容基本上都是围绕着“在 Ruby 语法的范围内到底能设计出多接近英语的 DSL”这个话题,从各种角度来说都让我觉得很有意思。例如,如果你问我,一个用来描述面包做法的 DSL 写成像图 7 这样好不好,说实话我还真没有什么自信。

recipe "Spicy Bread" do
  add 200.grams.of Flour  # 加入200克小麦粉
  add 1.lsp.of Nutmeg     # 加入一大勺肉豆蔻
#继续……
end
图 7 描述面包做法的 DSL

话说,如果忽略符号的话,作为英语来理解也确实没有太大问题,但越是接近自然语言,作为外行人就越容易纠结在一些微妙的差异上,例如到底要在哪里加上符号,顺序能不能调换等等。我觉得这正是 COBOL 曾经经历过的坎坷,但英语圈的人们却似乎有着不同的感触和理解。

接下来的一个要素是单位,也就是在图 7 的例子中出现的克(grams)、大勺(lsp)等等。

由于在 Ruby 中可以在已有的类中自由添加新的方法,利用这一点,在上面的例子中实际上是在整数类中定义了 grams 方法和 lsp 方法。

我第一次见到这样的扩展功能,是在 Ruby on Rails 10所包含的 ActiveSupport 库中。当时我看到“现在时间的 20 小时之前”居然能够写成“20.hours.ago”的时候感到非常震惊。实际上,是整数类中的 hours 方法对 20.hours 这一调用返回了 72000(3600 秒 ×20)这个值,而 ago 方法又返回了表示该秒数之前这一时刻的一个 Time 对象而已。

10 Ruby on Rails 简称 Rails,是用 Ruby 语言编写的一个非常有名的开源 Web 开发框架,它遵循 MVC 模式,提出了“不做重复的事”、“惯例优于配置”等理念,这些理念也被后来其他语言编写的一些 Web 开发框架(如 CakePHP、Zend 等)借鉴。

这样看来,Rails 不仅是一种 Web 应用程序框架,同时也可以说是以 Web 应用程序开发为对象领域的,以 Ruby 为基础的内部 DSL。

Glenn Vanderburg 所说的另外两个要素就是词汇和层次结构。前者的意思是,为目的领域定义了多少适用的方法,对必要方法的自动生成功能也包含在内。

例如,在 Rails 中,如果数据库的 users 表中包含 name 这一属性的话,那么就可以进行这样的调用:

User.find_by_name("松本")
其中 find_by_name 方法就是自动生成的。

层次结构可以理解为嵌套的上下文。

Vanderburg 举了图 8 这样的一个例子。这是使用 XmlMarkup 库的一个例子,将 Ruby 作为一种 DSL 来生成 XML,看起来可能比 XML 的代码要易读易写得多。由图 8 的代码,用 XmlMarkup 所生成的 XML 文件的内容如图 9 所示。

xml = Builder::XMLMarkup.new
xml.html {
  xml.head{
    xml.title("History")
  }
  xml.body {
    xml.h1("Header")
    xml.p("paragraph")
  }
}
图 8 用 XmlMarkup 这一 DSL 来生成 XML

<html>
  <head>
    <title>History</title>
  </head>
  <body>
    <h1>Header</h1>
    <p>paragraph</p>
  </body>
</html>
图 9 XmlMarkup 所生成的 XML

Sinatra

我觉得 Rails 具备 DSL 的性质并不是有意为之,而是“为提高生产效率而追求抽象化而水到渠成的结果”,但比 Rails 更新的 Web 应用程序框架中,出现了一些明显对 DSL 有所意识的项目,其中的代表就是 Sinatra。

Sinatra 是定位于较小规模 Web 应用程序的框架,用 Sinatra 编写的用来显示“Hello, world!”字符串的应用程序如图 10 所示。对于 Web 浏览器传送过来的“get /”请求应当如何响应,正是用 DSL 的形式来表达的。这种十分简洁的表达方式,以及应用了上下文和语句的代码风格,和 Rakefile、shoulda 这些以 Ruby 为基础的内部 DSL 可以说是有异曲同工之妙。

# hello_world.rb
require 'sinatra'

get '/' do
  "Hello world, it's #{Time.now} at the server!"
end
图 10 用 Sinatra 编写的 Web 版 Hello World

小结

DSL 是在 Ruby 界备受关注的一种方式,或者可能有点红过了头。DSL 这个称呼虽然只是最近才出现的,但实际上它已经是从数十年前开始就已经在使用的技术了。

不过,正如通过给一种设计模式起个名字能够提高其认知度,从而让更多的人来使用它一样,DSL 也可能因为被赋予了这个名字而获得了广泛的认知,从而对编程生产效率的提高做出贡献。今后,DSL 也可能作为判断设计优秀与否的重要指标,对软件开发产生巨大的影响。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文