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

“没有继承性的扩展,第 1 部分” 主要讨论了 Goovy、Scala 和 Clojure 中为现有类添加新方法的机制,这也是 Java 下一代语言实现无继承扩展的方法之一。本文将探讨 Clojure 的协议如何以创新的方法拓展 Java 扩展功能,为表达式问题提供出色的解决方案。
尽管这期文章主要关注可扩展性,但也会略为涉及一些允许 Clojure 和 Java 代码无缝互操作的 Clojure 特性。这两种语言有着根本性的差别(Java 是命令式、面向对象的;而 Clojure 是函数式的),但 Clojure 实现了一些便捷的特性,使 Clojure 能够在确保最小摩擦的前提下处理 Java 结构。
Clojure 协议回顾
协议是 Clojure 生态系统的重要组成部分。上一期文章 展示了如何使用协议向现有类添加方法。协议也能帮助 Clojure 模拟面向对象的语言的为人熟知的许多特性。例如,Clojure 可模拟面向对象的类 — 数据与方法的组合,方法是通过协议将记录 与函数 绑定在一起的。为了理解协议与记录之间的交互,首先必须介绍映射,这是作为 Clojure 中记录基础的核心数据结构。
映射与记录
在 Clojure 中,映射就是一组名称-值对的集合(其他语言中常见的概念)。例如,清单 1 中的 “读取-求值-打印” 循环 (REPL) 的第一步就是创建一个包含有关 Clojure 编程语言信息的映射:
清单 1. 与 Clojure 映射交互
user=> (def language {:name "Clojure" :designer "Hickey" })
#'user/language
user=> (get language :name)
"Clojure"
user=> (:name language)
"Clojure"
user=> (:designer language)
"Hickey"
Clojure 广泛使用映射,因此其中包含特殊的语法糖,可简化与映射的交互。为检索与某个键有关的值,您可以使用熟悉的 (get ) 函数。但 Clojure 会尽可能地简化此类常用操作。
在 Java 环境中,语言的源代码并非原生数据结构,必须对它进行分析和转换。在 Clojure(和其他 Lisp 变体)中,源代码表示属于 原生数据结构,比如列表,列表有助于解释语言中的奇怪语法。在 Lisp 解释器将列表作为源代码读取时,它会尝试着将列表的第一个元素解释为某些可调用 的元素,比如函数。因此在 清单 1 中,(:name language) 表达式将返回与 (get language :name) 表达式相同的结果。Clojure 之所以提供这种语法糖,是因为从映射中检索项目属于常用操作。
此外,在 Clojure 中,某些结构可放在函数调用插槽中,这扩展了可调用性(像调用函数一样调用这些结构的能力)。Java 程序只可以调用方法和内置语言语句。清单 1 展示了映射键(如 (:name language))在 Clojure 中可作为函数加以调用。映射本身也是可调用的;如果您认为替代语法 (language :name) 更容易阅读,也可以使用这种替代语法。Clojure 丰富的可调用图表使得这种语言更易于使用,从而减少了重复的语法(例如 Java 程序中常见的 get 和 set )。
然而,映射并不能完全模拟 JVM 类。Clojure 提供了其他方法来帮助您建模包括数据和行为在内的问题,更加无缝地集成底层 JVM。您可以创建对应于类似的底层 JVM 类且完整性各有不同的多种结构,包括类型 和记录 在内。您可以使用 (deftype ) 创建一个类型,通常用该类型来建模机械 结构。例如,如果您需要一个数据类型来持有 XML,那么很有可能会使用 (deftype MyXMLStructure) 表示 XML 内嵌的数据提取结构。在 Clojure 中,习惯于使用记录获得数据,信息记录 是应用程序的核心。为支持这种用法,Clojure 将在包含可调用性等特性的底层记录定义中自动包含大量接口。清单 2 中的 REPL 交互演示了记录的底层类和超类:
清单 2. 记录的底层类和超类
user=> (defrecord Person [name age postal])
user.Person

user=> (def bob (Person."Bob" 42 60601))
#'user/bob
user=> (:name bob)
"Bob"
user=> (class bob)
user.Person
user=> (supers (class bob))
#{java.io.Serializable clojure.lang.Counted java.lang.Object
clojure.lang.IKeywordLookup clojure.lang.IPersistentMap
clojure.lang.Associative clojure.lang.IMeta
clojure.lang.IPersistentCollection java.util.Map
clojure.lang.IRecord clojure.lang.IObj java.lang.Iterable
clojure.lang.Seqable clojure.lang.ILookup}
在 清单 2 中,我创建了一个名为 Person 的新记录,它包含用于 name、age 和 postal 代码的字段。我可以使用 Clojure 针对构造函数调用的语法糖来构造此类新记录(使用类名称加一个句点作为函数调用)。返回值为带有名称空间的实例。(默认情况下,所有 REPL 交互都发生在 user 名称空间内。)可调用性规则仍然存在,因此我可以使用 清单 1 展示的语法糖来访问记录的成员。
调用 (class ) 函数时,它将返回 Clojure 创建的名称空间和类名(可与 Java 代码交互)。我还可以使用 (supers ) 来访问 Person 的超 class。在 清单 2 的最后四行中,Clojure 实现了几个接口,包括 IPersistentMap 等可伸缩性接口,该接口允许使用 Clojure 的原生映射语法来处理类和对象。自动包含的一组接口是记录与类型之间的一个重要差别,类型不包含任何自动接口实现。
回页首
使用记录实现协议
Clojure 协议就是指定函数及其签名的指定集合。清单 3 中的定义将创建一个协议对象和一组多态协议函数:
清单 3. Clojure 协议
(defprotocol AProtocol
  "A doc string for AProtocol abstraction"
  (bar [this a] "optional doc string for aar function")
  (baz [this a] [this a b]
     "optional doc string for multiple-arity baz function"))
清单 3 中的函数对一个参数的类型进行分派,这使得它在该类型上具有多态性(此类型通常被命名为 this,以模拟 Java 上下文占位符)。因此,所有协议函数至少必须有一个参数。通常,协议使用驼峰式大小写混合格式命名;因为它们将在 JVM 级别上具体化 Java 接口,因此与 Java 命名规范保持一致能够简化互操作性。
记录可以实现协议,就像是在 Java 语言中实现接口一样。记录必须(将在运行时检查)实现与协议签名匹配的函数。在清单 4 中,我创建了一个实现 AProtocol 的记录:
清单 4. 实现协议
(defrecord Foo [x y]
   AProtocol
   (bar [this a] (min a x y))
   (baz [this a] (max a x y))
   (baz [this a b] (max a b x y)))

;exercising the record
(def f (Foo.1 200))
(println (bar f 4))
(println (baz f 12))
(println (baz f 10 2000))
在 清单 4 中,我创建了一个名为 Foo 的记录,它带有两个字段:x 和 y。为了实现协议,我必须包含匹配其签名的函数。实现协议后,我可以为对象的实例调用函数,就像调用普通函数一样。在函数定义中,我可以访问该记录的两个内部字段(x 和 y)以及函数参数。
回页首
协议扩展选项
作为一种轻松扩展现有类和层次结构的方法,协议在设计时便考虑到了表达式问题。(有关表达式文档的完整介绍,请参阅 上一期文章。)由于这些扩展是函数(就像 Clojure 中的其他内容一样),因此不会出现面向对象语言所固有的身份和继承问题。而且这种机制支持各种有用的扩展。
Clojure 是一种托管式语言:它被设计为(使用协议)在多种平台上运行,包括 .NET 和 JavaScript(通过 ClojureScript 编译器实现)。JavaScript 需要一种能够设置、卸除、加载和评估代码的环境。因此 ClojureScript 定义了 BrowserEnv 记录,用它为恰当的 JavaScript 环境(浏览器、REPL 或伪环境)处理生命周期函数,例如 setup 和 teardown。清单 5 给出了 BrowserEnv 的记录定义:
清单 5. ClojureScript 的 BrowserEnv 记录
(defrecord BrowserEnv []
  repl/IJavaScriptEnv
  (-setup [this]
    (do (require 'cljs.repl.reflect)
        (repl/analyze-source (:src this))
        (comp/with-core-cljs (server/start this))))
  (-evaluate [_ _ _ js] (browser-eval js))
  (-load [this ns url] (load-javascript this ns url))
  (-tear-down [_]
    (do (server/stop)
        (reset! server/state {})
        (reset! browser-state {}))))
在 IJavaScriptEnv 协议中定义的生命周期方法支持实现程序(如浏览器)访问通用接口。在函数名称开头处使用连字符(例如,(-tear-down ))是 ClojureScript(而非 Clojure)的规范。
表达式问题解决方案的另一个目标是能够为现有层次结构添加新特性,同时保证无需重新编译或 “触及” 现有层次结构。在版本 1.5 中,Clojure 引进了名为 Reducers 的高级集合库。这个库添加了适用于多种集合类型的自动并发处理。为了利用 Reducers 库,现有类型必须实现该库的一个方法,即 coll-fold。由于采用了协议和便捷的 extend-protocol 宏(该宏允许您一次性将一个协议扩展到多种类型),(coll-fold ) 函数可跨多种核心类型进行使用,如清单 6 所示:
清单 6. Reducers 将 (coll-fold ) 连接到多种类型
(extend-protocol CollFold
 nil
 (coll-fold
  [coll n combinef reducef]
  (combinef))

 Object
 (coll-fold
  [coll n combinef reducef]
  ;;can't fold, single reduce
  (reduce reducef (combinef) coll))

 clojure.lang.IPersistentVector
 (coll-fold
  [v n combinef reducef]
  (foldvec v n combinef reducef))

 clojure.lang.PersistentHashMap
 (coll-fold
  [m n combinef reducef]
  (.fold m n combinef reducef fjinvoke fjtask fjfork fjjoin)))
清单 6 中的 (extend-protocol ) 调用将 CollFold 协议(其中只包含一个 (coll-fold )方法)连接到 nil、Object、IPersistentVector 和 PersistentHashMap 类型。即便 nil(Clojure 中等同于 Java 语言 null 的变体)在这个库中也可以正常使用,处理空集合的常见边缘情况。Reducers 库还会连接到两个核心集合类,即 IPersistentVector 和 IPersistentHasMap,以便在这些集合层次结构的顶层附近添加 Reducer 功能。
Clojure 采用一组优雅的构建块支持便捷而强大的扩展。由于这种语言基于函数,而非基于类,所以部分开发人员可能不习惯其代码组织方式 —— Clojure 未将类作为主要组织原则。Clojure 的代码组织方式与 Java 大体相同,但内容比 Java 精简一些。Java 中有包、类和方法,而 Clojure 中有名称空间(大致对应于包)和函数(大致对应于方法)。Clojure 协议还会在必要时生成原生 Java 接口,以便开发人员用它们实现互操作性。在 Clojure 中,最便捷的功能是在组件边界定义协议,将类似的函数和协议放在一个名称空间内。Clojure 不具备类这种信息隐藏机制,但您可以定义名称空间私有函数(使用 (defn- ) 函数定义)。
Clojure 在名称空间中的代码组织使得整洁、居中的扩展成为可能。观察 清单 6 中的 CollFold 协议,它出现在 Clojure 源代码的 reducers.clj 文件中。此文件是在 Clojure 1.5 版本中添加的,协议、新类型和扩展均处于此文件中。利用协议扩展,您就可以再次利用核心类型(例如 Object),并添加 Reducer 功能,部分此类功能是通过 reducers 名称空间内的名称空间私有函数来实现的。 Clojure 以极高的精确度为现有层次结构添加了重要的新行为,而且不会提高复杂度,还能将所有相关细节保存在一个位置。
(extend-type ) 宏类似于 (extend-protocol ) 宏;使用 (extend-type ) 宏,您可以同时为一个类型添加多个协议。清单 7 展示了 ClojureScript 如何向 arrays 添加集合功能:
清单 7. 向 JavaScript 数组添加集合功能
(extend-type array
  ICounted
  (-count [a] (alength a))

  IReduce
  (-reduce [col f] (array-reduce col f))
  (-reduce [col f start] (array-reduce col f start)))
在 清单 7 中,ClojureScript 需要 JavaScript 数组来响应 Clojure 函数,例如 (count ) 和 (reduce )。(extend-type ) 宏允许在一个位置上实现多种协议。Clojure 期望集合响应 count 而非 length,因此连接了 ICounted 协议和函数,并添加了适当的方法别名。
协议的具体化不需要记录。就像 Java 中的匿名对象一样,协议也可以具体化并内联使用,如清单 8 所示:
清单 8. 协议的内联具体化
(let [z 42
      p (reify AProtocol
       (bar [_ a] (min a z))
       (baz [_ a] (max a z)))]
  (println (baz p 12)))
在 清单 8 中,我使用了一个 let 块来创建两个本地绑定:x 和 p,即内联协议定义。在创建匿名协议时,我仍然可以访问本地作用域:其中使用 z 作为参数是合法的,因为 z 处于此 let 块的作用域内。通过这种方式,具体化的协议可以像闭包块一样封装其环境。请注意,我并未完整实施协议;baz 函数的自变量版本并不完整。不同于 Java 接口,协议实现是可选的。如果 Clojure 需要的协议方法并不存在,它不会在编译时强制使用协议,而是生成一条运行时错误。
回页首
结束语
本期的 Java 下一代 文章探索了如何将 Java 中像类和接口这样的公共规范映射为 Clojure 中的结构。此外还探索了 Clojure 中对协议的各种用法,以及 Clojure 如何轻松优雅地解决表达式问题,还介绍了几种实际变体。在下一期文章中,我将探索 Groovy 中的混入类 (mixin),总结无继承扩展 系列。
参考资料
学习
高效率程序员(Neal Ford,O'Reilly Media,2008 年):Neal Ford 的这本书具体介绍了本系列的多个主题。
Clojure:Clojure 是一种现代化的函数式 Lisp,运行在 JVM 之上。
Scala:Scala 是基于 JVM 的一种现代化函数式语言。
Groovy:Groovy 是 Java 语言的动态变体,采用了更新的语法和功能。
“表达式问题”:Philip Walder 这篇在 1998 年撰写的文章并未发布,其中详细介绍了表达式问题。
“使用 Clojure 1.2 解决表达式问题”(Stuart Sierra,developerWorks,2010 年 12 月):了解 Clojure 的表达式问题解决方案的更多信息。
探索 Java 平台的替代语言:跟随这个学习路线图,研究关于各种替代性 JVM 语言的 developerWorks 内容。
语言设计者的笔记本:在这个 developerWorks 系列中,Java 语言架构师 Brian Goetz 探讨了一些语言设计问题,这些问题为 Java 语言在 Java SE 7、Java SE 8 和更改版本中的演化带来了挑战。
ClojureScript:进一步了解 ClojureScript Clojure-to-JavaScript 编译器。
函数式思维:在 developerWorks 上 Neal Ford 的专栏系列中探索函数式编程。
Java 下一代:没有继承性的扩展,第 1 部分:了解 Groovy、Scala 和 Clojure 如何将行为融入到类中 (Neal Ford,developerWorks,2013 年 6 月):探索 Groovy、Scala 和 Clojure 中的扩展机制,包括类别类、ExpandoMetaClass、隐式转换和协议,借助它们来使用 Java 下一代语言扩展 Java 类。
Java 下一代: Groovy、Scala 和 Clojure 中的共同点,第 1 部分(Neal Ford,developerWorks,2013 年 3 月):使用 Java 的下一代语言(Groovy、Scala 和 Clojure)解决了无法在 Java 语言中重载运算符的问题。
Java 下一代:Groovy、Scala 和 Clojure 中的共同点,第 2 部分(Neal Ford,developerWorks,2013 年 4 月):通过 Java 下一代语言所提供的语法便捷性减少样板代码和复杂性。
Java 下一代:Groovy、Scala 和 Clojure 中的共同点,第 3 部分(Neal Ford,developerWorks,2013 年 5 月):对比 Groovy、Scala 和 Clojure,了解如何改进一些瑕疵,比如异常、语句与表达式,以及围绕 null 代码的边缘情况。
Java 下一代:Java 下一代语言(Neal Ford,developerWorks,2013 年 1 月):在这篇对 Java 下一代语言和它们的优势进行概括的文章中,探索了 3 种下一代 JVM 语言(Groovy、Scala 和 Clojure)的异同。
该作者的更多文章(Neal Ford,developerWorks,2005 年 6 月至今):了解 Groovy、Scala、Clojure、函数式编程、架构、设计、Ruby、Eclipse 和其他 Java 相关技术。
developerWorks Java 技术专区:查看数百篇关于 Java 编程各个方面的文章。
获得产品和技术
下载 IBM 产品评估版本,并开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。
讨论

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



转自:http://www.ibm.com/developerworks/cn/java/j-jn6/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值