扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
Bruce Tate, 总裁, J2Life, LLC
2006 年 4 月 04 日
DSL 是专门解决特定于域问题的语言。通过更接近问题的操作,DSL 可以提供在通用语言中可能找不到的好处。Java 世界中充满了 DSL。属性文件、Spring 上下文、标注的某种用法以及 Ant 任务,都是 DSL 的示例。
在开始研究其他像 Ruby 这样的语言的时候,我开始理解到 Java 语言目前对于 DSL 还没有良好的把握。在这篇文章中,将看到 Ruby 使用的四种集成干净的 DSL 的技巧。然后,将看到在 Java 语言中可能存在的选项是什么。
|
虽然您可能不知道,但实际上您无处不遇到 DSL,从日常生活到使用的应用程序,到您编写的程序。在法庭上,可以看到速记员用 DSL 迅速地进行记录。音乐使用几种不同的标注来描述音量、音调和每个音的时长,采用一种适合特定乐器的格式。(我使用吉它六线谱,里面每条线都代表吉它上的一根弦。)使用 DSL 是因为它们比口述或笔录更能有效地解决问题。
在使用日常的应用程序时,也在使用 DSL。最好的示例是电子表格。编写电子表格,要比使用最简单的会计程序还要容易。电子表格的 DSL 从根本上改变了为特定问题进行编程的实质。
回头来看,Java 也在到处使用 DSL:
Java 语言并不特别擅长特定于域的语言,因为这个语言很难按照对 DSL 开发人员来说最有吸引力的方式进行扩展。这就是为什么 XML 这么泛滥的一个原因。XML 是可扩展的,Java 和它的集成很好,可以容易地构建解释它的工具,而且它也不需要和 Java 类一起编译。但是 XML 对于人类阅读来说很不友好。所以,可以看到对于在 Java 语言中 XML 的过度使用有广泛的抱怨。
在跨越边界 系列的 第一篇文章 中,您看到了活动记录(Ruby on Rails 背后的持久化引擎)。在这篇文章中,我又回到活动记录,因为它在多个地方对 DSL 概念进行了精彩的应用:
has_many :people
来构建与另一个数据库支持的对象的一对多关系映射。People
的活动记录类,就会拥有与数据库中每个列对应的属性。Fixnum
这样的类以提供对域友好的体验。随着继续阅读本文,将看到让这些技巧成为可能的 Ruby 特性。您将真正体会到在 Ruby 和 Java 操作方式之间的区别。要跟随本文一起编写代码,需要安装 Ruby 和 Ruby on Rails,其中包含了活动记录(请参阅 参考资料)。
Ruby 语法开放的结构和符号的包含,使得定义词汇相当容易。可以使用方法、符号和类来形成词汇。请输入 irb
来启动 Ruby 解释器。输入清单 1 中的代码。(清单 1 显示了输入的内容和 Ruby 中的结果。只需要输入黑体的代码。)
|
在清单 1 中,创建了叫作 Person
的类,它有两个实例变量分别叫作 name
和 email
。请特别注意 attr_accessor :name, :email
这一行。有两个概念应当引起注意:
清单 1 中的 attr_accessor :name, :email
语句创建两个属性,分别带有 getter 和 setter 存取器。attr
实际上是个方法调用 —— 是 Ruby 语言本身元编程的精彩示例。Java 开发人员习惯于在类体中看到方法声明,而不习惯看到方法调用。这个方法调用把方法和实例变量添加到 Person
类中。
如果没有 attr_accessor :name, :email
,就必须为每个需要的属性输入清单 2 的代码:
|
清单 2 —— Ruby 版的 getter 和 setter —— 看起来应当有点儿熟悉。name=
实际上是个方法名称,而 @
加在所有实例变量前作为前缀,但剩下的就与 Java 的 getter 和 setter 很类似了。
如果不用清单 2 中的代码,也可以用 @attr
的另一个版本来创建带有 getter、setter 或两者都有的属性。
第二个值得注意的概念是符号。可以把 :email
当成名为 email 的东西。Ruby 符号像字符串,但是是不可修改的字符串,而且只有一个实例。只能使用一个 :email
符号。
现在看起来像下面这样的活动记录代码应当让您有点儿感觉了:
|
has_one
是个方法,:department
是个符号,活动记录只是把它解释成类的名称。因为 Ruby 并不强制要求在方法参数两边使用括号,还因为 Rails 可以使用专门为活动记录设计的符号和方法名称,所以这个词汇畅通无阻。
活动记录充分利用了 Ruby 的另一个特性。会经常看到带有可选参数的 Ruby 方法,可选参数是一个默认为空的哈希 map。可以用这种方式模拟命名参数。例如,活动记录方法 belongs_to
的定义看起来像这样:
|
现在可以把选项传递给 belongs_to
来优化它的行为:
|
在 Ruby 中,用 key => value
指定哈希 map 的条目。意思很清楚:想让活动记录覆盖默认值(department_id
,根据命名规范)而采用 department_number
。因为可以修剪选项的名称来满足语法的要求,所以 DSL 就得到了另一个强大的特性:可选的扩展。下面需要的能力是用自己的词汇来扩展 Ruby 语言。
Ruby 是种动态语言,所以向现有类(甚至指定类的实例)添加行为很容易。现在先使用这项技术来针对某个域修饰现有类,然后再根据词汇扩展现有类。
罗马数字的使用不太频繁,但是在某些上下文中会有用。我们并不想直接把罗马数字添加到 Ruby 的 Fixnum
基类,但是它们对于特定于域的语言可能是有用的。可以把 to_roman
方法添加到 Fixnum
类,这个方法把 fixnum
转换成罗马数字。这件事做起来极为容易。只要再次打开类定义,并定义新方法即可。清单 3 显示了一个粗糙的罗马数字处理方法:
|
一旦理解了分号分隔了两个不同的 Ruby 语句,清单 3 就简单了。当我想让两个不同的想法挂在一起的时候,就经常用这种方式。可以用这项技术添加或修改任何 Ruby 类的定义。这一特殊实现的好处在于使用模型。可以把它粘贴到一个文件中,并在 Ruby 解释器中使用它,如清单 4 所示:
|
Rails 利用这个能力处理像时间测量之类的事情。例如,在 Rails 应用程序中,可以说 10.days
,或 2.hours.ago
,或 5.minutes.from_now
。使用这个技术,可以把现有 Ruby 词汇扩展到自己的域中,处理类似测量、转换或其他语法组合的事情。最终结果是一个干净漂亮的 Ruby 核心类,带有一些扩展,提供特定于域的类,可以在域的上下文中做正确的事。
在得到了词汇和扩展类的能力之后,下一步是根据词汇动态地 扩展类。在 清单 1 中的 attr
就是这种技术的示例。现在将介绍如何实现它(感谢 Glenn Vanderburg;请参阅 参考资料)。清单 5 显示了初步的尝试:
|
这个示例稍微复杂了一些。self.class
返回 Person
的类。然后 class_eval
在这个类的上下文环境下计算以下字符串。第一行定义 getter,第二行定义 setter。这个示例把 name
属性添加到 Person
。
清单 5 有两个主要问题。首先,需要显式地调用 my_attr
。还不能从类中调用它,因为它还没有定义。其次,硬编码的 name
应当是个符号。第一个问题可以通过声明一个模块并从这个模块进行继承来解决。第二个问题可以通过传递进一个符号来解决。清单 6 显示了第二次尝试:
|
清单 6 看起来有点儿神秘,但是不用担心。可以在一点儿帮助下理解这段代码。刚才只改变了三件事:
Person
类,而是打开了超类 —— Ruby 的 Class
。name
,而是传递进一个叫作 symbol
的参数。用 #{symbol}
代替了 name
。Ruby 用代表符号的字符串替换 #{symbol}
。class_eval
代替了 self.class.class_eval
。代码已经在类中操作了,所以不需要得到 self.class
。 要查看它的工作,可以在 Ruby 解释器中输入清单 7 中黑体部分的代码:
|
正如所期望的,可以把行为添加到任何现有类。现在看到了怎样才能把行为绑定到可以添加到类的附加功能上。这项技术就是活动目录添加高级概念(例如 belongs_to
和 has_many
)的方式。但是活动记录没有把行为添加到类,而是添加到叫作 ActiveRecord::Base
的模块。
现在已经看到了一些相当精密的功能的作用,但是 Ruby 还能做更多支持 DSL 的事。
有时,想根据外部情况把方法添加到类。例如,假设想在 Ruby 中表示罗马数字。要把它们与字符串分开,可以用 Roman.III
的形式把数字 3 表示成罗马数字。要为每个可能的罗马数字都向 Roman
添加类方法,是不现实的,而且使用 Ruby 时也不需要这么做。可以利用一个小技巧。
在 Ruby 中,在遗漏了一个方法时,Ruby 就会调用 method_missing
方法。可以覆盖它来提供罗马数字,如清单 8 所示:
|
这个代码相当简单,但是确实使用了 Java 程序员不熟悉的一些 Ruby 特性。由于覆盖了 method_missing
,所以只要这个类的客户调用一个不存在的方法,Ruby 就会调用这个方法。下面说明细节:
name
代表方法名 *args
代表遗漏方法的参数 to_s
把它转换成 String
。X
、V
和 I
的出现,还不能得到它们的值。I
(1)、V
(5)、X
(10)、 L
(50)或 C
(100)。对于 DSL,这个技术极为强大。活动记录使用这个功能实现动态查找器。活动记录没有为每个列实际地添加查找器,而是使用了 method_missing
。使用这个策略,活动记录不仅能匹配一个列,还能匹配列的组合。例如,把 name
和 email
列添加到 people
表,可以支持 Person
类的 People.find_by_name_and_email
查找器。像这样的细节使得活动记录的用户体验非常舒服。它们也让活动记录的实现非常简洁而有意义,所以在活动记录做的工作不符合自己的要求时,随时可以实现自己的补丁。
在使用 Java 语言时,选项就非常有限了。元编程更困难,所以很少能够得到活动记录那样的体验。但是如果真的急需 DSL,还是有些选项的。而且不用总是求助于 XML 或标注。下面是一些常用的方法:
这些主意,每个都有一系列 developerWorks 文章,所以我在这里对它们就不做太多详细介绍了,但是有一点我要提一下。如果需要在 Java 语言中使用 DSL,需要问自己四个问题:
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。