Java 下一代: 没有继承性的扩展,第 1 部分

了解 Groovy、Scala 和 Clojure 如何将行为融入到类中

Groovy、Scala 和 Clojure 提供了许多扩展机制,但继承几乎是 Java™ 语言的惟一选择。这一期将介绍类别类、ExpandoMetaClass、隐式转换和协议,借助它们来使用 Java 下一代语言扩展 Java 类。

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks Inc.

Photo of Neal FordNeal Ford 是一家全球性的 IT 咨询公司 ThoughtWorks 的主管、软件架构师和 Meme Wrangler。他还设计并编写了一些应用程序、教材、杂志文章、课件和视频/DVD演示文稿,他是多种技术书籍的作者或编辑,其中包括最近出版的这本 Presentation Patterns。他的工作重点是设计和构建大型企业级应用程序。他还是全球范围开发者大会上的一位国际知名的演讲者。您可以查看 他的网站



2013 年 8 月 15 日

关于本系列

Java 传承的是平台,而不是语言。有超过200 种语言可以在JVM 上运行,它们之中不可避免地会有一种语言最终将取代 Java 语言,成为编写 JVM 程序的最佳方式。本系列将探讨 3 种下一代 JVM 语言:Groovy、Scala 和 Clojure,比较并对比新的功能和范例,让 Java 开发人员对自己近期的未来发展有大体的认识。

Java 语言的设计有目的地进行了一定的删减,以避免前代产品中已发现的一些问题。例如,Java 语言的设计人员感觉 C++ 中的多重继承性带来了太多复杂性,所以它们选择不包含该特性。事实上,他们在该语言中很少构建扩展性选项,仅依靠单一继承和接口。

其他语言(包括 Java 下一代语言)存在巨大的扩展潜力。在本期和接下来的两期文章中,我将探索扩展 Java 类而不涉及继承性的途径。在本文中,您会了解如何向现有类添加方法,无论是直接还是通过语法糖 (syntactic sugar)。

表达式问题

表达式问题是最近的计算机科学历史上的一个众所周知的观察结果,首创于贝尔实验室的 Philip Wadler 的一篇未发表的论文(参见 参考资料)。(Stuart Sierra 在其 developerWorks 文章 “通过 Clojure 1.2 解决表达式问题” 中出色地解释了它。在这篇文章中,Wadler 说道:

表达式问题是老问题的新名字。我们的目标是通过案例定义数据类型,在这里,在不重新编译现有代码的情况下,您可以将新的案例添加到数据类型和数据类型的新函数中,同时保留静态类型安全(例如,没有转换)。

换句话说,您如何向一个分层结构中的类添加功能,而不求助于类型转换或 if 语句?

我们将通过一个简单的例子来表明表达式问题在真实世界中的表现形式。假设您公司始终假设应用程序中的长度单位为,没有在您的类中为任何其他长度单位构建任何功能。但是,有一天,您公司与一家竞争对手合并了,而这个竞争对手始终假设长度单位为英尺

解决该问题的一种方法是,通过使用转换方法扩展 Integer,使两种格式之间的切换变得无关紧要。现代语言提供了多种解决方案来实现此目的;在本期中,我将重点介绍其中的 3 种:

  • 开放类
  • 包装器类
  • 协议

Groovy 的类别和 ExpandoMetaClass

Groovy 包含两种使用开放类 扩展现有的类的不同方式,“重新开放” 一个类定义来实现更改(例如添加、更改或删除方法)的能力。

类别类

类别类(一种借鉴自 Objective-C 的概念)是包含静态方法的常规类。每个方法至少接受一个参数,该参数表示方法扩充的类型。如果希望向 Integer 添加方法,例如我需要接受该类型作为第一个参数的静态方法,如清单 1 所示:

清单 1. Groovy 的类别类
class IntegerConv {
  static Double getAsMeters(Integer self) {
    self * 0.30480
  }

  static Double getAsFeet(Integer self) {
    self * 3.2808
  }
}

清单 1 中的 IntegerConv 类包含两个扩充方法,每个扩充方法都接受一个名为 self(一个通用的惯用名称)的 Integer 参数。要使用这些方法,我必须将引用代码包装在一个 use 代码块中,如清单 2 所示:

清单 2. 使用类别类
@Test void test_conversion_with_category() {
  use(IntegerConv) {
    assertEquals(1 * 3.2808, 1.asFeet, 0.1)
    assertEquals(1 * 0.30480, 1.asMeters, 0.1)
  }
}

清单 2 中有两个特别有趣的地方。首先,尽管 清单 1 中的扩展方法名为 getAsMeters(),但我将它称为 1.asMeters。Groovy 围绕 Java 中的属性的语法糖使我能够执行 getAsMeters() 方法,好像它是名为 asMeters 的类的一个字段一样。如果我在扩展方法中省略了 as,对扩展方法的调用需要使用空括号,就像 1.asMeters() 中一样。一般而言,我喜欢更干净的属性语法,这是编写特定于域的语言 (DSL) 的一种常见技巧。

清单 2 中第二个需要注意的地方是对 asFeetasMeters 的调用。在 use 代码块中,我同等地调用新方法和内置方法。该扩展在 use 代码块的词法范围内是透明的,这很好,因为它限制了扩充(有时是一些核心)类的范围。

ExpandoMetaClass

类别是 Groovy 添加的第一种扩展机制。但事实证明对构建 Grails(基于 Groovy 的 Web 框架)而言,Groovy 的词法范围限制太多了。由于不满类别中的限制,Grails 的创建者之一 Graeme Rocher 向 Groovy 添加了另一种扩展机制:ExpandoMetaClass

ExpandoMetaClass 是一种懒惰实例化的扩展持有者,它可从任何类 “成长” 而来。清单 3 展示了如何使用 ExpandoMetaClass,为我的 Integer 类实现我的扩展:

清单 3. 使用 ExpandoMetaClass 扩展 Integer
class IntegerConvTest{

  static {
    Integer.metaClass.getAsM { ->
      delegate * 0.30480
    }

    Integer.metaClass.getAsFt { ->
      delegate * 3.2808
    }
  }

  @Test void conversion_with_expando() {
    assertTrue 1.asM == 0.30480
    assertTrue 1.asFt == 3.2808
  }
}

清单 3 中,我使用 metaClass holder 添加 asMasFt 属性,采用与 清单 2 相同的命名约定。对 metaclass 的调用出现在测试类的一个静态初始化器中,因为我必须确保扩充操作在遇到扩展方法之前发生。

类别类和 ExpandoMetaClass 都在内置方法之前调用扩展类方法。这使您能够添加、更改或删除现有方法。清单 4 给出了一个示例:

清单 4. 取代现有方法的扩展类
@Test void expando_order() {
  try {
    1.decode()
  } catch(NullPointerException ex) {
    println("can't decode with no parameters")
  }
  Integer.metaClass.decode { ->
    delegate * Math.PI;
  }
  assertEquals(1.decode(), Math.PI, 0.1)
}

清单 4 中的第一个 decode() 方法调用是一个内置的静态 Groovy 方法,它设计用于更改整数编码。正常情况下,它会接受一个参数;如果调用时没有任何参数,它将抛出 NullPointerException。但是,当我使用自己的 decode() 方法扩充 Integer 类时,它会取代原始类。


Scala 的隐式转换

Scala 使用包装器类 来解决表达式问题的这个方面。要向一个类添加一个方法,可将它添加到一个帮助类中,然后提供从原始类到您的帮助器的隐式转换。在执行转换之后,您就可以从帮助器隐式地调用该方法,而不是从原始类调用它。清单 5 中的示例使用了这种技术:

清单 5. Scala 的隐式转换
class UnitWrapper(i: Int) {
  def asFt = {
    i * 3.2808
  }

  def asM = {
    i * 0.30480
  }
}

implicit def unitWrapper(i:Int) = new UnitWrapper(i)

println("1 foot = " + 1.asM + " meters");
println("1 meter = " + 1.asFt + "foot")

清单 5 中,我定义了一个名为 UnitWrapper 的帮助器类,它接受一个构造函数参数和两个方法:asFtasM。在拥有转换值的帮助类后,我创建了一个 implicit def,实例化一个新的 UnitWrapper。要调用该方法,可以像调用原始类的一个方法那样调用它,比如 1.asM。当 Scala 未在 Integer 类上找到 asM 方法时,它会检查是否存在隐式转换,从而允许将调用类转换为一个包含目标方法的类。像 Groovy 一样,Scala 拥有语法糖,因此我能够省略方法调用的括号,但这是一种语言特性而不是命名约定。

Scala 中的转换帮助器通常是 object 而不是类,但我使用了一个类,因为我希望传递一个值作为构造函数参数(object 不允许这么做)。

Scala 中的隐式转换是一种扩充现有类的精妙且类型安全的方式,但不能向开放类一样,使用这种机制更改或删除现有方法。


Clojure 的协议

Clojure 采用了另一种方法来解决表达式问题的这个方面,那就是结合使用 extend 函数和 Clojure 协议 抽象。协议在概念上类似于一个 Java 接口:一个没有实现的方法签名集合。尽管 Clojure 实质上不是面向对象的,而是偏向于函数,但您可以与类进行交互(并扩展它们),并将方法映射到函数。

为了扩展数字以添加转换,我定义了一个协议,它包含我的两个函数(asFasM)。我可使用该协议 extend 一个现有类(比如 Number)。extend 函数接受目标类作为第一个参数,接受该协议作为第二个参数,以及一个使用函数名为键并使用实现(以匿名函数形式)为值的映射。清单 6 显示了 Clojure 单位转换:

清单 6. Clojure 的扩展协议
(defprotocol UnitConversions
  (asF [this])
  (asM [this]))

(extend Number
  UnitConversions
  {:asF (fn [this] (* this 3.2808))
   :asM #(* % 0.30480)})

我可以在 Clojure REPL(interactive read-eval-print loop,交互式读取-重新运算-打印循环)上使用新的扩展来验证该转换:

user=> (println "1 foot is " (asM 1) " meters")
1 foot is  0.3048  meters

清单 6 中,两个转换函数的实现演示了匿名函数声明的两种语法变体。每个函数只接受一个参数(asF 函数中的 this)。单参数函数很常见,以至于 Clojure 为它们的创建提供了语法糖,如 AsM 函数中所示,其中 % 是参数占位符。

协议创建了一种将方法(以函数形式)添加到现有类中的简单解决方案。Clojure 还包含一些有用的宏,使您能够将一组扩展整合在一起。例如,Compojure Web 框架(参见 参考资料)使用协议扩展各种类型,以便它们 “知道” 如何呈现自身。清单 7 显示了来自 Compojure 中的 Renderable 的一段代码:

清单 7. 通过协议扩展许多类型
(defprotocol Renderable
  (render [this request]
    "Render the object into a form suitable for the given request map."))

(extend-protocol Renderable
  nil
  (render [_ _] nil)
  String
  (render [body _]
    (-> (response body)
        (content-type "text/html; charset=utf-8")))
  APersistentMap
  (render [resp-map _]
    (merge (with-meta (response "") (meta resp-map))
           resp-map))
  IFn
  (render [func request]
    (render (func request) 
  ; . . .

清单 7 中,Renderable 协议是使用单个 render 函数来定义的,该函数接受一个值和一个请求映射作为参数。Clojure 的 extend-protocol 宏(它可用于将协议定义分组到一起)接受类型和实现对。在 Clojure 中,您可使用下划线代替不关心的参数。在 清单 7 中,这个定义的可看见部分为 nilStringAPersistentMapIFn(Clojure 中的函数的核心接口)提供了呈现指令。(该框架中还包含其他许多类型,但为节省空间,清单中省略了它们。)可以看到这在实践中非常有用:对于您可能需要呈现的所有类型,您可将语义和扩展放在一起定义。


结束语

在本期中,我介绍了表达式问题,剖析了 Java 下一代语言如何处理以下方面:现有类的干净扩展。每种语言都使用一种不同的技术(Groovy 使用开放类,Scala 使用包装器类,而 Clojure 实现了协议)来实现类似的结果。

但是,表达式问题比类型扩充更深刻。在下一期中,我将继续讨论使用其他协议功能、特征和 mix-in 的扩展。

参考资料

学习

获得产品和技术

  • 下载 IBM 产品评估版,动手使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

  • 加入 developerWorks 社区。探索由开发人员推动的 博客、论坛、群组和维基,并与其他 developerWorks 用户进行交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=940432
ArticleTitle=Java 下一代: 没有继承性的扩展,第 1 部分
publish-date=08152013