副标题#e#
相识 Groovy、Scala 和 Clojure 如何将行为融入到类中
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 的种别类
1 2 3 4 5 6 7 8 9 | class IntegerConv { static Double getAsMeters(Integer self) { static Double getAsFeet(Integer self) { |
清单 1 中的 IntegerConv 类包括两个扩充要领,每个扩充要领都接管一个名为
self(一个通用的习用名称)的 Integer 参数。要利用这些要领,我必需将引用代码包
装在一个 use 代码块中,如清单 2 所示:
清单 2. 利用种别类
1 2 3 4 5 6 | @Test void test_conversion_with_category() { assertEquals(1 * 3.2808, 1.asFeet, 0.1) 0.30480, 1.asMeters, 0.1) |
清单 2 中有两个出格有趣的处所。首先,尽量 清单 1 中的扩展要领名为
getAsMeters(),但我将它称为 1.asMeters。Groovy 环绕 Java 中的属性的语法糖使我
可以或许执行 getAsMeters() 要领,仿佛它是名为 asMeters 的类的一个字段一样。假如我
在扩展要领中省略了 as,对扩展要领的挪用需要利用空括号,就像 1.asMeters() 中一
样。一般而言,我喜欢更清洁的属性语法,这是编写特定于域的语言 (DSL) 的一种常见
能力。
清单 2 中第二个需要留意的处所是对 asFeet 和 asMeters 的挪用。在 use 代码块
中,我同等地挪用新要领和内置要领。该扩展在 use 代码块的词法范畴内是透明的,这
很好,因为它限制了扩充(有时是一些焦点)类的范畴。
#p#分页标题#e##p#副标题#e#
ExpandoMetaClass
种别是 Groovy 添加的第一种扩展机制。但事实证明对构建 Grails(基于 Groovy
的 Web 框架)而言,Groovy 的词法范畴限制太多了。由于不满种别中的限制,Grails
的建设者之一 Graeme Rocher 向 Groovy 添加了另一种扩展机制:ExpandoMetaClass。
ExpandoMetaClass 是一种懒惰实例化的扩展持有者,它可从任何类 “生长” 而来
。清单 3 展示了如何利用 ExpandoMetaClass,为我的 Integer 类实现我的扩展:
清单 3. 利用 ExpandoMetaClass 扩展 Integer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class IntegerConvTest{ static { Integer.metaClass.getAsM { -> 0.30480 Integer.metaClass.getAsFt { -> 3.2808 @Test void conversion_with_expando() { 0.30480 == 3.2808 |
在 清单 3 中,我利用 metaClass holder 添加 asM 和 asFt 属性,回收与 清单 2
沟通的定名约定。对 metaclass 的挪用呈此刻测试类的一个静态初始化器中,因为我必
须确保扩充操纵在碰着扩展要领之前产生。
种别类和 ExpandoMetaClass 都在内置要领之前挪用扩展类要领。这使您可以或许添加、
变动或删除现有要领。清单 4 给出了一个示例:
清单 4. 代替现有要领的扩展类
1 2 3 4 5 6 7 8 9 10 11 | @Test void expando_order () { (NullPointerException ex) { with no parameters”) Integer.metaClass.decode { -> } (), Math.PI, 0.1) |
清单 4 中的第一个 decode() 要领挪用是一个内置的静态 Groovy 要领,它设计用
于变动整数编码。正常环境下,它会接管一个参数;假如挪用时没有任何参数,它将抛
出 NullPointerException。可是,当我利用本身的 decode() 要领扩充 Integer 类时
,它会代替原始类。
Scala 的隐式转换
Scala 利用包装器类 来办理表达式问题的这个方面。要向一个类添加一个要领,可
将它添加到一个辅佐类中,然后提供从原始类到您的辅佐器的隐式转换。在执行转换之
后,您就可以从辅佐器隐式地挪用该要领,而不是从原始类挪用它。清单 5 中的示例使
用了这种技能:
清单 5. Scala 的隐式转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class UnitWrapper(i: Int) { def asM = { implicit def unitWrapper(i:Int) = new UnitWrapper(i) println(“1 foot = ” + 1.asM + ” meters”); 1.asFt + “foot”) |
在 清单 5 中,我界说了一个名为 UnitWrapper 的辅佐器类,它接管一个结构函数
参数和两个要领:asFt 和 asM。在拥有转换值的辅佐类后,我建设了一个 implicit
def,实例化一个新的 UnitWrapper。要挪用该要领,可以像挪用原始类的一个要领那样
挪用它,好比 1.asM。当 Scala 未在 Integer 类上找到 asM 要领时,它会查抄是否存
在隐式转换,从而答允将挪用类转换为一个包括方针要领的类。像 Groovy 一样,Scala
拥有语法糖,因此我可以或许省略要领挪用的括号,但这是一种语言特性而不是定名约定。
Scala 中的转换辅佐器凡是是 object 而不是类,但我利用了一个类,因为我但愿传
递一个值作为结构函数参数(object 不答允这么做)。
Scala 中的隐式转换是一种扩充现有类的精妙且范例安详的方法,但不能向开放类一
样,利用这种机制变动或删除现有要领。
#p#分页标题#e#Clojure 的协议
Clojure 回收了另一种要领来办理表达式问题的这个方面,那就是团结利用 extend
函数和 Clojure 协议 抽象。协议在观念上雷同于一个 Java 接口:一个没有实现的方
法签名荟萃。尽量 Clojure 实质上不是面向工具的,而是方向于函数,但您可以与类进
行交互(并扩展它们),并将要领映射到函数。
为了扩展数字以添加转换,我界说了一个协议,它包括我的两个函数(asF 和 asM)
。我可利用该协议 extend 一个现有类(好比 Number)。extend 函数接管方针类作为
第一个参数,接管该协议作为第二个参数,以及一个利用函数名为键并利用实现(以匿
名函数形式)为值的映射。清单 6 显示了 Clojure 单元转换:
清单 6. Clojure 的扩展协议
1 2 3 4 5 6 7 8 | (defprotocol UnitConversions (extend Number this 3.2808))
|
我可以在 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. 通过协议扩展很多范例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | (defprotocol Renderable (render [this request] “Render the object into a form suitable for the given request map.”)) (extend-protocol Renderable String body) “text/html; charset=utf-8”))) (merge (with-meta (response “”) (meta resp-map)) IFn request] (render (func request) |
在 清单 7 中,Renderable 协议是利用单个 render 函数来界说的,该函数接管一
个值和一个请求映射作为参数。Clojure 的 extend-protocol 宏(它可用于将协议界说
分组到一起)接管范例和实现对。在 Clojure 中,您可利用下划线取代不体贴的参数。
在 清单 7 中,这个界说的可瞥见部门为 nil、String、APersistentMap 和 IFn
(Clojure 中的函数的焦点接口)提供了泛起指令。(该框架中还包括其他很多范例,
但为节减空间,清单中省略了它们。)可以看到这在实践中很是有用:对付您大概需要
泛起的所有范例,您可将语义和扩展放在一起界说。
竣事语
在本期中,我先容了表达式问题,分解了 Java 下一代语言如那里理惩罚以下方面:现有
类的清洁扩展。每种语言都利用一种差异的技能(Groovy 利用开放类,Scala 利用包装
器类,而 Clojure 实现了协议)来实现雷同的功效。
可是,表达式问题比范例扩充更深刻。在下一期中,我将继承接头利用其他协议成果
、特征和 mix-in 的扩展。
本栏目