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

探索 Clojure 协议

Java™ 语言受制于扩展机制的固有限制,主要依靠继承和接口。Groovy、Scala 和 Clojure 提供了其他许多扩展替代方案。这期文章进一步探索了 Clojure 将协议用作扩展机制的方法。

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

Photo of Neal FordNeal Ford is Director, Software Architect, and Meme Wrangler at ThoughtWorks, a global IT consultancy. He is also the designer and developer of applications, instructional materials, magazine articles, courseware, and video/DVD presentations, and he is the author or editor of books spanning a variety of technologies, including the most recent Presentation Patterns. He focuses on designing and building large-scale enterprise applications. He is also an internationally acclaimed speaker at developer conferences worldwide. Check out his website.



2013 年 9 月 23 日

关于本系列

Java 历来都是一种平台,而非语言。有超过 200 种语言在 JVM 上运行,其中必将有一种语言取代 Java 语言,成为 JVM 编程的最佳方法。本系列介绍了三种下一代 JVM 语言:Groovy、Scala 和 Clojure,文章对比了新功能和范式,旨在帮助 Java 开发人员窥见未来。

没有继承性的扩展,第 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 程序中常见的 getset )。

然而,映射并不能完全模拟 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 的新记录,它包含用于 nameagepostal 代码的字段。我可以使用 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 的记录,它带有两个字段:xy。为了实现协议,我必须包含匹配其签名的函数。实现协议后,我可以为对象的实例调用函数,就像调用普通函数一样。在函数定义中,我可以访问该记录的两个内部字段(xy)以及函数参数。


协议扩展选项

作为一种轻松扩展现有类和层次结构的方法,协议在设计时便考虑到了表达式问题。(有关表达式文档的完整介绍,请参阅 上一期文章。)由于这些扩展是函数(就像 Clojure 中的其他内容一样),因此不会出现面向对象语言所固有的身份和继承问题。而且这种机制支持各种有用的扩展。

Clojure 是一种托管式语言:它被设计为(使用协议)在多种平台上运行,包括 .NET 和 JavaScript(通过 ClojureScript 编译器实现)。JavaScript 需要一种能够设置、卸除、加载和评估代码的环境。因此 ClojureScript 定义了 BrowserEnv 记录,用它为恰当的 JavaScript 环境(浏览器、REPL 或伪环境)处理生命周期函数,例如 setupteardown。清单 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 )方法)连接到 nilObjectIPersistentVectorPersistentHashMap 类型。即便 nil(Clojure 中等同于 Java 语言 null 的变体)在这个库中也可以正常使用,处理空集合的常见边缘情况。Reducers 库还会连接到两个核心集合类,即 IPersistentVectorIPersistentHasMap,以便在这些集合层次结构的顶层附近添加 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 块来创建两个本地绑定:xp,即内联协议定义。在创建匿名协议时,我仍然可以访问本地作用域:其中使用 z 作为参数是合法的,因为 z 处于此 let 块的作用域内。通过这种方式,具体化的协议可以像闭包块一样封装其环境。请注意,我并未完整实施协议;baz 函数的自变量版本并不完整。不同于 Java 接口,协议实现是可选的。如果 Clojure 需要的协议方法并不存在,它不会在编译时强制使用协议,而是生成一条运行时错误。


结束语

本期的 Java 下一代 文章探索了如何将 Java 中像类和接口这样的公共规范映射为 Clojure 中的结构。此外还探索了 Clojure 中对协议的各种用法,以及 Clojure 如何轻松优雅地解决表达式问题,还介绍了几种实际变体。在下一期文章中,我将探索 Groovy 中的混入类 (mixin),总结无继承扩展 系列。

参考资料

学习

获得产品和技术

  • 下载 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=946490
ArticleTitle=Java 下一代: 没有继承性的扩展,第 2 部分
publish-date=09232013