扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
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领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者