跨越边界: 活动记录和 Java 编程中特定于域的语言

来源:百度文库 编辑:神马文学网 时间:2024/05/26 20:24:53
掌握您的域

文档选项

将此页作为电子邮件发送

拓展 Tomcat 应用

下载 IBM 开源 J2EE 应用服务器 WAS CE 新版本 V1.1
级别: 初级
Bruce Tate (bruce.tate@j2life.com), 总裁, J2Life, LLC
2006 年 4 月 04 日
Java™ 编程的世界充满了特定于域的语言(DSL),但是在 Java 语言中用来构建 DSL 的选项是有限的。使用 Ruby 则不是这样。在这篇文章中,将学习 Ruby 提供的一些漂亮方式,可以集成干净的 DSL,让您大开眼界,用一种新的参考框架来审视 Java 的选项。
DSL 是专门解决特定于域问题的语言。通过更接近问题的操作,DSL 可以提供在通用语言中可能找不到的好处。Java 世界中充满了 DSL。属性文件、Spring 上下文、标注的某种用法以及 Ant 任务,都是 DSL 的示例。
在开始研究其他像 Ruby 这样的语言的时候,我开始理解到 Java 语言目前对于 DSL 还没有良好的把握。在这篇文章中,将看到 Ruby 使用的四种集成干净的 DSL 的技巧。然后,将看到在 Java 语言中可能存在的选项是什么。

在跨越边界 系列中,作者 Bruce Tate 提出了这样一个主张:今天的 Java 程序员通过学习其他技术和语言,会得到很好的帮助。Java 技术是所有开发项目的明显最好选择的情况,在编程领域中已经发生了变化。其他框架正在影响 Java 框架构建的方式,而从其他语言学到的概念也有助于 Java 编程。对 Python(或 Ruby、Smalltalk 等等)代码的编写,可能改变 Java 编码的方式。
这个系列介绍的编程概念和技术,与 Java 开发有根本的不同,但却可以直接适用于 Java 编程。在某些情况下,需要整合这些技术来利用它们。在其他情况下,可以直接应用这些概念。比起其他语言和框架能够影响 Java 社区的开发人员、框架甚至基本方式的理念来说,单独的工具并不那么重要。
虽然您可能不知道,但实际上您无处不遇到 DSL,从日常生活到使用的应用程序,到您编写的程序。在法庭上,可以看到速记员用 DSL 迅速地进行记录。音乐使用几种不同的标注来描述音量、音调和每个音的时长,采用一种适合特定乐器的格式。(我使用吉它六线谱,里面每条线都代表吉它上的一根弦。)使用 DSL 是因为它们比口述或笔录更能有效地解决问题。
在使用日常的应用程序时,也在使用 DSL。最好的示例是电子表格。编写电子表格,要比使用最简单的会计程序还要容易。电子表格的 DSL 从根本上改变了为特定问题进行编程的实质。
回头来看,Java 也在到处使用 DSL:
JSP 使得构建定制的用户界面更容易。 SQL 代表数据库操作。 属性文件代表程序的配置。 XML 描述数据。 XML 描述程序配置,例如在 EJB、Hibernate 或 Spring 中。 XML 描述动作,例如 Ant 任务或某种引擎中的业务规则。
Java 语言并不特别擅长特定于域的语言,因为这个语言很难按照对 DSL 开发人员来说最有吸引力的方式进行扩展。这就是为什么 XML 这么泛滥的一个原因。XML 是可扩展的,Java 和它的集成很好,可以容易地构建解释它的工具,而且它也不需要和 Java 类一起编译。但是 XML 对于人类阅读来说很不友好。所以,可以看到对于在 Java 语言中 XML 的过度使用有广泛的抱怨。
在跨越边界 系列的第一篇文章 中,您看到了活动记录(Ruby on Rails 背后的持久化引擎)。在这篇文章中,我又回到活动记录,因为它在多个地方对 DSL 概念进行了精彩的应用:
特定于域的语句结构和词汇表。 活动记录构建了一个用 Ruby 对象包装关系数据库的词汇表。例如,在数据库支持的对象中,可以用 has_many :people 来构建与另一个数据库支持的对象的一对多关系映射。
扩展类的行为。 根据命名规范,声明叫作 People 的活动记录类,就会拥有与数据库中每个列对应的属性。
修饰现有类型。 Rails 通常修饰 Fixnum 这样的类以提供对域友好的体验。
动态地扩展词汇表。 活动记录提供了一些惊喜,例如根据数据库的结构添加定制查找器。
英语建模。 活动记录根据上下文修改类的复数形式。
随着继续阅读本文,将看到让这些技巧成为可能的 Ruby 特性。您将真正体会到在 Ruby 和 Java 操作方式之间的区别。要跟随本文一起编写代码,需要安装 Ruby 和 Ruby on Rails,其中包含了活动记录(请参阅参考资料)。




回页首
Ruby 语法开放的结构和符号的包含,使得定义词汇相当容易。可以使用方法、符号和类来形成词汇。请输入 irb 来启动 Ruby 解释器。输入清单 1 中的代码。(清单 1 显示了输入的内容和 Ruby 中的结果。只需要输入黑体的代码。)
irb(main):001:0> class Person irb(main):002:1> attr_accessor :name, :email irb(main):003:1> end => nil irb(main):004:0> person = Person.new => # irb(main):005:0> person.name = "Elvis" => "Elvis" irb(main):006:0>
在清单 1 中,创建了叫作 Person 的类,它有两个实例变量分别叫作 name 和 email。请特别注意 attr_accessor :name, :email 这一行。有两个概念应当引起注意:
类定义中的方法调用 符号的使用
清单 1 中的 attr_accessor :name, :email 语句创建两个属性,分别带有 getter 和 setter 存取器。attr 实际上是个方法调用 —— 是 Ruby 语言本身元编程的精彩示例。Java 开发人员习惯于在类体中看到方法声明,而不习惯看到方法调用。这个方法调用把方法和实例变量添加到 Person 类中。
如果没有 attr_accessor :name, :email,就必须为每个需要的属性输入清单 2 的代码:
def name=(value) @name = value end def name return @name end
清单 2 —— Ruby 版的 getter 和 setter —— 看起来应当有点儿熟悉。name= 实际上是个方法名称,而 @ 加在所有实例变量前作为前缀,但剩下的就与 Java 的 getter 和 setter 很类似了。
如果不用清单 2 中的代码,也可以用 @attr 的另一个版本来创建带有 getter、setter 或两者都有的属性。
第二个值得注意的概念是符号。可以把 :email 当成名为 email 的东西。Ruby 符号像字符串,但是是不可修改的字符串,而且只有一个实例。只能使用一个 :email 符号。
现在看起来像下面这样的活动记录代码应当让您有点儿感觉了:
class Manager < ActiveRecord::Base has_one :department end
has_one 是个方法,:department 是个符号,活动记录只是把它解释成类的名称。因为 Ruby 并不强制要求在方法参数两边使用括号,还因为 Rails 可以使用专门为活动记录设计的符号和方法名称,所以这个词汇畅通无阻。
活动记录充分利用了 Ruby 的另一个特性。会经常看到带有可选参数的 Ruby 方法,可选参数是一个默认为空的哈希 map。可以用这种方式模拟命名参数。例如,活动记录方法 belongs_to 的定义看起来像这样:
def belongs_to(association_id, options = {})
现在可以把选项传递给 belongs_to 来优化它的行为:
class Manager < ActiveRecord::Base has_one :department, :foreign_key => "department_number" end
在 Ruby 中,用 key => value 指定哈希 map 的条目。意思很清楚:想让活动记录覆盖默认值(department_id,根据命名规范)而采用 department_number。因为可以修剪选项的名称来满足语法的要求,所以 DSL 就得到了另一个强大的特性:可选的扩展。下面需要的能力是用自己的词汇来扩展 Ruby 语言。




回页首
Ruby 是种动态语言,所以向现有类(甚至指定类的实例)添加行为很容易。现在先使用这项技术来针对某个域修饰现有类,然后再根据词汇扩展现有类。
罗马数字的使用不太频繁,但是在某些上下文中会有用。我们并不想直接把罗马数字添加到 Ruby 的 Fixnum 基类,但是它们对于特定于域的语言可能是有用的。可以把 to_roman 方法添加到 Fixnum 类,这个方法把 fixnum 转换成罗马数字。这件事做起来极为容易。只要再次打开类定义,并定义新方法即可。清单 3 显示了一个粗糙的罗马数字处理方法:
class Fixnum def to_roman value = self str = "" (str << "C"; value = value - 100) while (value >= 100) (str << "XC"; value = value - 90) while (value >= 90) (str << "L"; value = value - 50) while (value >= 50) (str << "XL"; value = value - 40) while (value >= 40) (str << "X"; value = value - 10) while (value >= 10) (str << "IX"; value = value - 9) while (value >= 9) (str << "V"; value = value - 5) while (value >= 5) (str << "IV"; value = value - 4) while (value >= 4) (str << "I"; value = value - 1) while (value >= 1) str end end
一旦理解了分号分隔了两个不同的 Ruby 语句,清单 3 就简单了。当我想让两个不同的想法挂在一起的时候,就经常用这种方式。可以用这项技术添加或修改任何 Ruby 类的定义。这一特殊实现的好处在于使用模型。可以把它粘贴到一个文件中,并在 Ruby 解释器中使用它,如清单 4 所示:
irb(main):001:0> load ‘to_roman.rb‘ => true irb(main):002:0> 10.to_roman => "X" irb(main):003:0> 199.to_roman => "CXCIX" irb(main):004:0>
Rails 利用这个能力处理像时间测量之类的事情。例如,在 Rails 应用程序中,可以说 10.days ,或 2.hours.ago,或 5.minutes.from_now。使用这个技术,可以把现有 Ruby 词汇扩展到自己的域中,处理类似测量、转换或其他语法组合的事情。最终结果是一个干净漂亮的 Ruby 核心类,带有一些扩展,提供特定于域的类,可以在域的上下文中做正确的事。




回页首
在得到了词汇和扩展类的能力之后,下一步是根据词汇动态地 扩展类。在清单 1 中的 attr 就是这种技术的示例。现在将介绍如何实现它(感谢 Glenn Vanderburg;请参阅参考资料)。清单 5 显示了初步的尝试:
class Person def my_attr self.class.class_eval "def name; @name; end" self.class.class_eval "def name=(val); @name = val; end" end end
这个示例稍微复杂了一些。self.class 返回 Person 的类。然后 class_eval 在这个类的上下文环境下计算以下字符串。第一行定义 getter,第二行定义 setter。这个示例把 name 属性添加到 Person。
清单 5 有两个主要问题。首先,需要显式地调用 my_attr。还不能从类中调用它,因为它还没有定义。其次,硬编码的 name 应当是个符号。第一个问题可以通过声明一个模块并从这个模块进行继承来解决。第二个问题可以通过传递进一个符号来解决。清单 6 显示了第二次尝试:
class Module def my_attr(symbol) class_eval "def #{symbol}; @#{symbol}; end" class_eval "def #{symbol}=(value); @#{symbol} = value; end" end end
清单 6 看起来有点儿神秘,但是不用担心。可以在一点儿帮助下理解这段代码。刚才只改变了三件事:
没有声明新的 Person 类,而是打开了超类 —— Ruby 的 Class。
没有硬编码 name,而是传递进一个叫作 symbol 的参数。用 #{symbol} 代替了 name。Ruby 用代表符号的字符串替换 #{symbol}。
用 class_eval 代替了 self.class.class_eval。代码已经在类中操作了,所以不需要得到 self.class。
要查看它的工作,可以在 Ruby 解释器中输入清单 7 中黑体部分的代码:
irb(main):001:0> require "my_attr.rb" => true irb(main):002:0> class Person irb(main):003:1> my_attr :name irb(main):004:1> end => nil irb(main):005:0> person = Person.new => # irb(main):006:0> person.name = "Bruce" => "Bruce" irb(main):007:0> person.name => "Bruce"
正如所期望的,可以把行为添加到任何现有类。现在看到了怎样才能把行为绑定到可以添加到类的附加功能上。这项技术就是活动目录添加高级概念(例如 belongs_to 和 has_many)的方式。但是活动记录没有把行为添加到类,而是添加到叫作 ActiveRecord::Base 的模块。
现在已经看到了一些相当精密的功能的作用,但是 Ruby 还能做更多支持 DSL 的事。




回页首
有时,想根据外部情况把方法添加到类。例如,假设想在 Ruby 中表示罗马数字。要把它们与字符串分开,可以用 Roman.III 的形式把数字 3 表示成罗马数字。要为每个可能的罗马数字都向 Roman 添加类方法,是不现实的,而且使用 Ruby 时也不需要这么做。可以利用一个小技巧。
在 Ruby 中,在遗漏了一个方法时,Ruby 就会调用 method_missing 方法。可以覆盖它来提供罗马数字,如清单 8 所示:
class Roman def self.method_missing name, *args roman = name.to_s if(roman =~ /^[IVXLC]*$/) roman.gsub!("IV", "IIII") roman.gsub!("IX", "VIIII") roman.gsub!("XL", "XXXX") roman.gsub!("XC", "LXXXX") return(roman.count("I") + roman.count("V") * 5 + roman.count("X") * 10 + roman.count("L") * 50 + roman.count("C") * 100) else super(name, *args) end end end
这个代码相当简单,但是确实使用了 Java 程序员不熟悉的一些 Ruby 特性。由于覆盖了 method_missing,所以只要这个类的客户调用一个不存在的方法,Ruby 就会调用这个方法。下面说明细节:
使用两个参数: name 代表方法名 *args 代表遗漏方法的参数
name 是个符号,所以首先用 to_s 把它转换成 String。
用正则表达式进行数字是否罗马数字的合理猜测。
如果数字是罗马数字,就进行一系列替换,让罗马数字更容易处理。IV 是 4 ,IX 是 9,所以只计算 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 或标注。下面是一些常用的方法:
对于需求不太迫切的 DSL,可以使用 Java 类、方法和名称构建对英语友好的词汇,并通过消息调用做需要的事。
对于典型的 Java 用户,可以用 XML 构建自己的语言。XML 难以阅读,但是在某些情况下可能有用,并在 Java 世界中相当普遍。
对于已经要求 XML 的解决方案,可以使用 XML 的派生物来简化。Craig Walls 有一个贴子介绍了如何用 XBean 为 Spring 上下文做这件事(请参阅参考资料)。
可以使用 XML 的替代表示(例如 Relax NG)来简化 XML(请参阅参考资料)。
当 Java 代码和 XML 都不够用的时候,可以在 JVM 中嵌入一种语言。最好的方式是通过 BeanShell(请参阅参考资料)。
对于在 Java 应用程序中需要动态脚本的解决方案,可以利用已经有 BeanShell 集成的更加动态的语言。好的示例有 Jython、JRuby 和 Groovy(请参阅参考资料)。
可以从头开始构建 DSL。在 Java 语言中这很难做到,但是对于某些应用程序来说还是值得一做。
这些主意,每个都有一系列 developerWorks 文章,所以我在这里对它们就不做太多详细介绍了,但是有一点我要提一下。如果需要在 Java 语言中使用 DSL,需要问自己四个问题:
真的需要 DSL 么?通过 Java 技术的一些更聪明的使用,可能可以做到自己需要的事。
XML 或 XML 的派生物足够吗?Java 开发人员对于 XML 经常有点儿太热心了,但是有些派生物可以把事情略微简化。
可以在 Java 语言内部 使用其他语言吗?JRuby 正在越来越好,Groovy 正在就位,Jython 也正在变得更稳定。
从头开始构建 DSL 值得吗?用 Java 语言做这件事很难 —— 需要词法器、解析器和语法器。但是可以做到,可能值得做,具体取决于应用程序。




回页首
现在,我还没有许多确切的答案。Java 语言在 DSL 方面做得不太好,但是有必要了解在构建 DSL 时其他语言中可能需要什么。还应当注意新的研究。那些带来 IDEA IDE 的人们和一些其他公司正在开发一套叫作语言工作台 的产品(请参阅参考资料)。他们完全有可能革新我们的编码方式。这些想法 —— 许多超越了 Java 编程 —— 正在扩展 DSL 的边界。
下次,我将讨论与并行编程有关的问题。将看到 Erlang 作为软实时分布系统的一种可能的解决方案。
学习
“Rolling with Ruby on Rails” 和Learn all about Ruby on Rails:学习关于 Ruby 和 Rails 的更多内容,包括安装过程。
Ruby Quiz:Ruby Quiz 每周都会介绍一些用 Ruby 解决的不同编程问题。其中一个问题就是罗马数字的来回转换。
Metaprogramming Ruby: Domain-Specific Languages for Programmers:Glenn Vanderburg 在 2005 年 O‘Reilly 开放源码大会上的讲话是份关于元编程的精彩演讲,涉及在这篇文章看到的许多技术。
“alt.lang.jre: Feeling Groovy”(Andrew Glover,developerWorks,2004 年 8 月):请阅读关于 Groovy 的信息,Groovy 是种在 JVM 中工作得很好的脚本语言。
“alt.lang.jre: Take a shine to JRuby”(Michael Squillace 和 Barry Feigenbaum,developerWorks,2004 年 9 月):了解 JRuby,这是在 JVM 中编程的 Ruby 实现。
Advanced Control Flow - Continuations:Cocoon 中的延续是通过用 Rhino 嵌入特定于域的语言而实现的。
RELAX NG Compact Syntax Tutorial:Relax NG 是一个围绕 XML 方案的 DSL,更易阅读。
BeanShell:BeanShell 是一种把脚本集成进 Java 应用程序的环境。
Spring Simplified with XBean:在 Craig Wall 的 Spring-Loaded 博客中的这篇文章讨论了使用 XBean 简化 Spring 中的 XML 配置。
Language Oriented Programming: The Next Revolution?:Neal Ford 关于 DSL 的谈话帮助形成了作者对于 DSL 的思考。
“Language Workbenches: The Killer-App for Domain Specific Languages?”:Martin Fowler 对语言工作台的介绍。
Beyond Java (O‘Reilly,2005 年):作者的书,介绍了 Java 编码的成长和平稳期,以及可能在某些领域挑战 Java 平台的技术。
Java 技术专区:数百篇关于 Java 编程各方面的文章。
获得产品和技术
Ruby on Rails:请下载开放源码的 Ruby on Rails Web 框架。带有活动记录的 Ruby on Rails 从头到尾大量利用了特定于域的语言。
Ruby:从 Ruby 项目的 Web 站点得到它。
讨论
developerWorks blogs:加入 developerWorks 社区。



Bruce Tate 是位父亲、山地车手、皮艇手,住在德克萨斯州的奥斯汀。他是三本最畅销 Java 图书的作者,包括获得 Jolt 奖的 Better, Faster, Lighter Java。他最近推出了 Spring: A Developer‘s Notebook。他在 IBM 工作了 13 年,现在是 J2Life, LLC 顾问公司的创始人,在那里他专攻基于 Java 技术和 Ruby 的轻量级开发策略和架构。
跨越边界: 活动记录和 Java 编程中特定于域的语言 跨越边界: 研究活动记录 Java编程语言中notify和wait的具体应用 Java编程语言中notify和wait的具体应用 跨越边界: Java 模型以外的类型策略 跨越边界: Java 模型以外的类型策略 跨越边界: JavaScript 语言特性 三大编程语言的性能PK--Java, C/C++和Ruby JavaScript语言和Java语言的区别 JavaScript语言和Java语言的区别 跨越边界: 用 Haskell 研究函数性编程 跨越边界: 用 Haskell 研究函数性编程 跨越边界-IBM developerWorks 中国 : Java technology : 文档库 跨越边界-IBM developerWorks 中国 : Java technology : 文档库 涉及C语言中参数调用和参数传递机制的探讨_非常编程 专注编程 跨越边界: 动态类型语言中的 Web 开发策略 Java编程语言如何完善自己的缺点-Java频道-中国IT实验室 开发JAVA编程中字符串分割的两种方法 aop和aspectj - Java编程 aop和aspectj - Java编程 挑战Java开发优势的四种技术:动态语言,元编程等 Google将限制Python语言的应用 开发社区热议 - Python编程 - Java... JRuby:使Java和Ruby成为一家人 - 其他编程语言 - LUPA论坛 |开源社区 Linux交流中心 源码交换 公文是社会组织在公务活动中所形成的具有法定效力的和规范体式的凭证性信息记录。公文具有区别于图书、情报、档案等其他文献形式的特殊属性。公文写作中通常涉及到公文的主愉、材料、结构和格式