通过 Clojure 1.2 解决表达式问题

将先前存在的类型扩展到新方法,将先前存在的方法扩展到新类型

Clojure 专家 Stuart Sierra 向您介绍了 Clojure 1.2 中解决表达式问题的新功能,这是一个典型的编程难题。协议(Protocols) 允许您将先前存在的类型扩展到新的方法,数据类型(datatypes) 允许您将先前存在的方法扩展到新的类型 — 所有这些都不改变现有的代码。您还将看到 Java™ 接口和类如何与 Clojure 协议和数据类型交互。

Stuart Sierra, 开发人员, Relevance, Inc.

http://www.ibm.com/developerworks/i/p-ssierra.jpgStuart Sierra 是一位生活在纽约的演员/作家/编码人员。他是 Relevance, IncClojure/核心 团队的成员。Stuart 是 实用的 Clojure(Apress,2010年)的合著者。他获得哥伦比亚大学的计算机科学理科硕士学位和纽约大学戏剧文学学士学位。



2011 年 4 月 14 日

协议 是 Clojure 1.2 版 -即 JVM 的动态编程语言 - 引入的新功能。在不牺牲 JVM 方法调度出色性能的情况下,协议代表针对面向对象编程的方法,这比 Java 类层次更灵活。协议 — 及相关功能数据类型— 对所知道的表达式问题提供了解决方案。解决表达式问题使将先前存在的类型扩展到新方法成为可能,所有这些都不重新编译现有代码。

Clojure 基础

本文假设您熟悉编写和运行 Clojure(和 Java)程序的基础知识。有关 Clojure 的介绍,请参阅 developerWorks 文章 “Clojure 编程语言” 以及查看 参考资料

本文描述了表达式问题,显示了一些示例,然后演示 Clojure 的协议和数据类型如何解决它,其简化了某种编程挑战。您还将看到如何将 Clojure 协议和数据类型功能与 Java 类和接口集成在一起。

多种类型,一个接口

Clojure 的核心功能之一是其一般数据操作 API。少数的功能可用在所有 Clojure 内置类型上。例如,conj 函数(conjoin 的简写)将元素添加到任何集合,如以下 REPL 会话所示:

user> (conj [1 2 3] 4)
[1 2 3 4]
user> (conj (list 1 2 3) 4)
(4 1 2 3)
user> (conj {:a 1, :b 2} [:c 3])
{:c 3, :a 1, :b 2}
user> (conj #{1 2 3} 4)
#{1 2 3 4}

虽然每一个数据结构对 conj 函数的反应稍稍不同(list 在头部增长,vectors 在尾部增长,等等),但是它们都支持相同的 API。这是一个多态性 的经典示例 — 通过统一接口访问的许多类型。

多态性是一种强大的功能,也是现代编程语言的基础之一。Java 语言支持称为子类型多态性 的特定类型多态性,这意味着可访问某种类型(类)的实例,就像 它是其他类型的实例。

在特定条件下,这就意味着您可以通过一般接口(如 java.util.List)处理对象,而无需知道或关心对象是 ArrayListLinkedListStackVector 还是其他。java.util.List 接口定义必须履行所有类都声明以实现 java.util.List 的合同。


表达式问题

Bell 实验室的 Philip Wadler 在 1998 年通过电子邮件传阅的未发布的文章中创造了表达式问题 这个词(请参考 参考资料)。正如他所说的那样,“表达式问题是老问题的新名字。目标是通过案例定义数据类型,此处在不重新编译现有代码的情况下您可以将新的案例添加到数据类型和数据类型的新函数中,同时保留静态类型安全(例如,没有转换)。”

为说明表达式问题,Wadler 的文章使用了表的概念,其中类型为行,函数为列。面向对象的语言更容易添加新的行 — 也就是说,您类型扩展了已知的接口 — 如图 1 所示:

图 1. 面向对象的语言:轻松添加新的行(类型)
面向对象的语言:轻松添加新的行(类型)

图 1 中,java.util.List 接口中的每一列都表示一种方法。为简单起见,它包括四种方法:List.addList.getList.clearList.size。前四行中的每一行都表示实现 java.util.List 的一个类: ArrayListLinkedListStackVector。这些行和列交叉处的单元格表示每一种类的方法的现有实现(通过标准 Java 类库提供)。在底部添加的第五行表示您可以编写的实现 java.util.List 的新类。对于行上的每一个单元格来说,您都可以编写在 java.util.List 中相应方法的您自己的实现,特定于您的新类。

相对于面向对象语言,函数语言通常更容易添加新列 — 也就是说,在现有类型上操作新的函数。对于 Clojure 来说,这可能类似于图 2:

图 2. 函数语言:轻松添加新的列(函数)
函数语言:轻松添加新的列(函数)

图 2 是另一个表,类似于 图 1。在这里,列表示 Clojure 的标准集合 API 中的函数:conjnthemptycount。同样,行表示 Clojure 的内置集合类型:listvectormapset。这些行和列交叉处的单元格表示 Clojure 提供的这些函数的现有实现。通过定义新的函数,您可以向表添加新的列。假设您的新函数是用 Clojure 的内置函数编写的,则它将能够自动支持所有相同的类型。

Wadler 的文章谈论的是静态类型语言(如 Java 语言)。Clojure 是动态类型 — 在编译时,无需声明或了解特定类型的对象。但是这不意味着在 Clojure 没有表达式问题。实际上,它几乎没有改变。动态类型并不意味着 Clojure 没有 类型。它只是无需您提前声明所有类型。将旧函数扩展到新类型和将新函数扩展到旧类型的问题 — 在两个方向上扩展 Wadler 的表 — 仍然存在。

具体示例

表达式问题不只是关于抽象的类型(如列表和集)。如果您已经花费了大量的时间使用面向对象的语言,则您可能已经遇到了表达式问题的例子。本部分提供一个具体的、简化的、真实的案例。

假设您在 WidgetCo 的 IT 部门工作,这是一家邮购办公用品公司。WidgetCo 已经用 Java 语言编写了其自己的帐单和库存管理软件。

WidgetCo 的产品都是通过一个简单接口描述的:

package com.widgetco;

public interface Widget {
    public String getName();
    public double getPrice();
}

对于通过 WidgetCo 生产的每一种独特类型的部件,IT 部门的编程人员编写了实现 Widget 接口的类。

来自 WidgetCo 客户之一的订单会作为一个 Widget 对象列表来实现,并使用一个额外的方法来计算订单的总成本,即:

package com.widgetco;

public class Order extends ArrayList<Widget> {
    public double getTotalCost() { /*...*/ }
}

WidgetCo 在被 Amagalmated Thingamabobs Incorporated 收购以前都是一切正常。 Amalgamated 拥有自己的自定义账单系统,也是用 Java 语言编写的。其库存围绕一个抽象类,即Product,通过该类派生特定产品类:

package com.amalgamated;

public abstract class Product {
    public String getProductID() { /*...*/ }
}

在 Amalgamated,产品没有固定价格。相反,公司与每一个客户谈判以便按照给定价格交付特定数量的产品。此协议通过 Contract 类表示:

package com.amalgamated;

public class Contract {
    public Product getProduct() { /*...*/ }
    public int getQuantity()    { /*...*/ }
    public double totalPrice()  { /*...*/ }
    public String getCustomer() { /*...*/ }
}

合并以后,您在 Amagalmated 的新老板分派给您编写新应用程序的任务,以便为合并后的公司生成发票和货运清单。但是有一个问题:新系统必须处理用于 WidgetCo 和 Amalgamated Thingamabobs 的库存管理的现有 Java 代码 — com.widgetco.Widgetcom.widgetco.Ordercom.amalgamated.Productcom.amalgamated.Contract 类。太多的其他应用程序依赖于此代码,所以变更它有一定风险。

您刚刚遇到了表达式问题。


潜在的 “解决方案”

虽然面向对象的语言提供一些可能的方法以便解决表达式问题,但是每一个方法都有其缺点。您可能已经遇到了这些技术中每一个的例子。

从共同超类中继承

针对这类问题的传统的面向对象的解决方案是利用子类型多态性 — 也就是说,继承。如果两个类需要支持相同的接口,它们都应该扩展相同的超类。在我的示例中,com.widgetco.Ordercom.amalgamated.Contract 需要产生发票和货运单。理想情况下,这两个类将通过必要的方法实现某个接口:

public interface Fulfillment {
    public Invoice invoice();
    public Manifest manifest();
}

要做到这一点,您需要修改 OrderContract 的源代码,以便实现新的接口。但是这是此示例中的问题:您不能修改这些类的源代码,甚至不能重新编译它们。

多重继承

表达式问题的另一种方法是多重继承,即一个子类可扩展许多超类。您需要采购的一种一般表示形式,其可能是 com.widgetco.Ordercom.amalgamated.Contract。以下的伪代码显示了如果 Java 语言具有多重继承,则这可能是什么样子:

public class GenericOrder
        extends com.widgetco.Order, com.amalgamated.Contract
        implements Fulfillment {

    public Invoice invoice() { /*...*/ }

    public Manifest manifest() { /*...*/ }
}

但是 Java 语言不支持具体类的多重继承,这具有充分的理由:它会导致复杂的和有时难于预测的类继承。Java 语言支持 接口的多重继承,因此如果 OrderContract 都是接口,则您可以使此技术起作用。但令人遗憾的是,OrderContract 的最初作者没有足够的远见来把它们的设计建立在接口上。即使它们是,这也将不是针对表达式问题的真实解决方案,因为您不能将 Fulfillment 接口添加到现有的 OrderContract 类中。相反,您已经创建了新的 GenericOrder 类,其作为包装程序具有相同问题,下面会进行描述。

包装程序

另外一种流行的解决方案是编写围绕您想修改其行为的类的包装程序。通过引用原始类来构建包装程序,其将方法发送到该类。Order 类的 Java 包装程序如下所示:

public class OrderFulfillment implements Fulfillment {
    private com.widgetco.Order order;

    /* constructor takes an instance of Order */
    public OrderFulfillment(com.widgetco.Order order) {
        this.order = order;
    }

    /* methods of Order are forwarded to the wrapped instance */

    public double getTotalCost() {
        return order.getTotalCost();
    }

    /* the Fulfillment interface is implemented in the wrapper */
    public Invoice invoice() { /*...*/ }

    public Manifest manifest() { /*...*/ }
}

OrderFulfillment 是围绕 Order 的包装程序。包装程序类实现了我 早先 描述的 Fulfillment 接口。它还复制并转发 Order 定义的方法。因为 Order 可扩展 ArrayList<Widget>,包装程序类的正确实现还将需要复制并转发 java.util.ArrayList 的所有方法。

一旦您创建了另一个也实现 Fulfillmentcom.amalgamated.Contract 包装程序,您就已经满足了任务的需求 — 但是越来越复杂。编写包装程序类很乏味。(java.util.ArrayList 有超过 30 种方法。) 更糟的是,它们破坏了您所希望的类的某些行为。OrderFulfillment,虽然其实现与 Order 相同的方法,但是并不是真正的 Order。您不能通过声明为 Order 的指针来访问它,您也不能将它传递到另外一个希望以 Order 作为参数的方法。通过添加包装程序类,您打破了子类型多态性。

更糟的是,包装程序类破坏了身份。在 OrderFulfillment 上包装的 Order 不再是同一个对象。您无法将针对 OrderOrderFulfillment 与 Java == 运算符作比较且希望它返回 true。如果您尝试在 OrderFulfillment 中覆盖 Object.equals 方法,从而其 “等于” 其 Order,您就打破了 Object.equals 的合同,其指定相等必须是对称的。因为 OrderOrderFulfillment 一无所知,所以在传递 OrderFulfillment 时,其 equals 方法将总是返回 false。如果您定义了 OrderFulfillment.equals 使其在传递 Order 时可返回 true,您就打破了 equals 的对称,会在其他类中导致一连串失败,如内置式 Java 集合类,其依赖于这种行为。这意味着:不要弄乱身份。

开放类

Ruby 和 JavaScript 语言有助于在面向对象的编程中普及开放式类 的理念。在定义时,开放式类并不只限于一组被实现的方法。在任何时间任何人都可以 “重新打开” 该类以便添加新的方法,或甚至替代现有的方法。

开放类允许了高度的灵活性和重用性。一般类可以扩展,具有特定于使用这些类的不同位置的功能。实现某一方面行为的方法组可以在混合式 中聚集在一起,该混合式被添加到任何需要此行为的类中。如果本文的示例是用 Ruby 或 JavaScript 编写的,则您可以简单地重新打开 OrderContract 类以便添加您需要的方法。

开放类的缺点,除了在大多数编程语言中的不存在性(包括 Java 语言),是由其本身的灵活性而带来的确定性的缺乏。如果您在类上定义 invoice 方法,则您没有办法知道该类的一些其他用户会不会定义不同的、不兼容的方法,还命名为 invoice。这就是名称冲突 的问题,这在使用开放类和无方法的命名空间机制的语言中很难避免。原因是这种技术被称为 “猴子补丁”。虽然它简单、易于理解,但是它后来几乎总是有问题。

条件和重载

表达式问题的最常用解决方案之一是普通老式的 if-then-else 逻辑。使用条件语句和类型检查,您要列举所有可能的情况并适当地处理每一种。对于发票示例来说,在 Java 语言中,您可以用静态方法实现:

public class FulfillmentGenerator {
    public static Invoice invoice(Object source) {
        if (source instanceof com.widgetco.Order) {
            /* ... */
        } else if (source instanceof com.amalgamated.Contract) {
            /* ... */
        } else {
            throw IllegalArgumentException("Invalid source.");
        }
    }
}

像包装程序类一样,虽然此技术满足了您的需要,但是有其自己的缺点。If-else 块链更加混乱和缓慢,您必须处理更多类型。此实现是封闭的:在您编译 FulfillmentGenerator 类以后,在不编辑源代码和重新编译的情况下,您无法将 invoice 方法扩展到新的类型。表达式问题的条件解决方案根本不是真实的解决方案,只是一个尝试,将导致将来更多的维护工作。

在本例中,通过为不同类型重载 invoice 方法,您可以避免条件逻辑:

public class FulfillmentGenerator {
    public static Invoice invoice(com.widgetco.Order order) { 
        /* ... */
    }

    public static Invoice invoice(com.amalgamated.Contract contract) {
        /* ... */
    }
}

虽然这样可完成同样的事情且比有条件的版本更有效,但是它是封闭的:在不修改 FulfillmentGenerator 源代码的情况下,您不可以为不同的类型添加新的 invoice 实现。在面对继承层次时,它也变得不可预测。假设您想为 OrderContract 的子类添加 invoice 的实现。只有 Java 语言专家可以告诉您将实际调用方法的哪个版本,而它可能不是您想要的那个。

虽然 Java 语言中表达式问题的真实解决方案确实 存在(请参考 参考资料),但是它们比您想要处理来解决简单业务问题所希望的要复杂得多。总的来说,我在这里提出的所有非解决方案都会失败,因为它们将类型与其他事情混为一谈:继承、身份或命名空间。Clojure 将单独处理每一个问题。


协议

Clojure 1.2 引入了协议。这并不是一个新的概念 — 计算机科学家们在 20 世纪 70年代就在研究相似的概念 — Clojure 的实现非常灵活,可以在保持其主机平台(即 JVM)性能的同时解决表达式问题。

理论上讲,协议类似 Java 接口。它定义了一组方法名称和其参数签名,但是没有实现。发票示例可能看起来如清单 1:

清单 1. Fulfillment 协议
(ns com.amalgamated)

(defprotocol Fulfillment
  (invoice [this] "Returns an invoice")
  (manifest [this] "Returns a shipping manifest"))

每一个协议方法至少采用一个参数,通常称为 this。像 Java 方法一样,“在” 对象上调用协议方法,对象类型确定使用哪种方法的实现。不同于 Java 语言,Clojure 需要 this 来声明为函数的明确参数。

协议不同于其方法存在的接口,作为普通函数,目前可以定义它们。清单 1 的示例定义了名为 invoicemanifest 的 Clojure 函数,两个都位于 com.amalgamated 命名空间中。当然,那些函数也没有任何实现,所以调用它们将只抛出异常。

协议允许您以案例为基础为其方法提供实现。使用名为 extend 的函数,您可将协议扩展 到新的类型。Extend 函数采用数据类型、协议和方法实现的映射。我将在 下一部分 解释数据类型,但是现在,只假设数据类型是普通 Java 类。您可以扩展 Fulfillment 协议以便处理旧 Order 类,如清单 2 所示:

清单 2. 扩展 Fulfillment 协议以便处理旧 Order
(extend com.widgetco.Order
  Fulfillment
  {:invoice (fn [this]
              ... return an invoice based on an Order ... )
   :manifest (fn [this]
               ... return a manifest based on an Order ... )})

请注意将您传递到 extend 函数的映射从关键字映射到匿名函数。关键字是协议中方法的名称;函数就是那些方法的实现。通过调用 extend,您告诉 Fulfillment 协议,“在这里就是用来在 com.widgetco.Order 类型上实现这些方法的代码。”

您可以为 Contract 类做相同的事情,如清单 3 所示:

清单 3. 扩展 Fulfillment 协议以便处理 Contract
(extend com.amalgamated.Contract
  Fulfillment
  {:invoice (fn [this]
              ... return an invoice based on a Contract ... )
   :manifest (fn [this]
               ... return a manifest based on a Contract ... )})

就这样,您具有多态 invoicemanifest 函数,可以在一个 Order 或一个 Contract 上调用且作出正确操作。您已经解决了表达式问题比较困难部分:在不修改或重新编译任何现有代码的情况下,已经将新的 invoicemanifest 函数添加到了先前就存在的 OrderContract 类型中。

与在开放类方法中不同,您没有修改 OrderContract 类。您也没有阻止其他代码定义其自己的具有不同实现的 invoice manifest 方法。在 com.amalgamated 中为您的 invoicemanifest 方法提供名称空间;在其他名称空间中定义的方法永远不会发生冲突。

Clojure(作为 Lisp),使用 来简化一些复杂或重复的语法。使用内置式 extend-protocol 宏,您可以在一个块上编写所有方法实现,如清单 4 所示:

清单 4. 使用 extend-protocol
(extend-protocol Fulfillment
  com.widgetco.Order
    (invoice [this]
      ... return an invoice based on an Order ... )
    (manifest [this]
      ... return a manifest based on an Order ... )
  com.amalgamated.Contract
    (invoice [this]
      ... return an invoice based on a Contract ... )
    (manifest [this]
      ... return a manifest based on a Contract ... ))

此宏代码扩大到要 extend 的两个调用,如 清单 2清单 3 中所示。


数据类型

协议是一个功能强大的工具:它们有效地为您提供将新方法插入现有类的能力,而没有名称冲突且无需修改原始代码。但是它们只解决一半的表达式问题,即 Wadler 表的 “新列”。您如何在 Clojure 中将 “新行” 添加到表中?

答案就是数据类型。在面向对象环境中,数据类型起与类的角色相同:它们封装了状态(字段)和行为(方法)。然而,主流面向对象的语言(如 Java 语言)倾向于混淆类在面向对象的设计中填充的不同角色。Clojure 的数据类型被划分为两种不同的角色。

结构化数据:defrecord

类的一种使用是作为结构化数据的容器。这就是几乎每一个面向对象的编程教材开始的地方:您具有包含类似 NameAge 字段的 Person 类。是否直接访问字段或通过 getter/setter 方法是无关紧要的;类本身在 C 上充当 struct 角色。其目的是存放一组在逻辑上相关的值。JavaBeans 就是用于存放结构化数据的类的示例。

将类用于结构化数据的问题就是为了访问此数据每一个类都有其自己不同接口,通常通过 getter/setter 方法。Clojure 支持统一接口,因此鼓励在映射中存储结构化数据。Clojure 的映射实现 — HashMapArrayMapStructMap — 都支持在本文一开始就讨论过的一般 数据操作接口。但是映射缺乏一些结构化数据的预期功能:它们没有真实的 “类型”(在不添加元数据的情况下),不像类那样,它们不能通过新的行为扩展。

Defrecord 宏在 Clojure 1.2 中随协议引入,可用于为结构化数据创建结合了映射和类功能的容器。

返回到本文的运行示例,假设 Amalgamated Thingamabobs Incorporated 的管理层决定统一其客户的购买流程。从这时开始,新产品订单将通过 PurchaseOrder 对象表示,且旧的 OrderContract 类将逐渐淘汰。

PurchaseOrder 将具有 “日期”、“客户” 和 “产品” 属性。它还必须提供 invoicemanifest 的实现。作为 Clojure defrecord,它看上去如清单 5 所示:

清单 5. PurchaseOrder 数据类型
(ns com.amalgamated)

(defrecord PurchaseOrder [date customer products]
  Fulfillment
    (invoice [this]
      ... return an invoice based on a PurchaseOrder ... )
    (manifest [this]
      ... return a manifest based on a PurchaseOrder ... ))

遵循名称 PurchaseOrder 的矢量定义了数据类型的字段。字段后面,defrecord 允许您为协议方法编写嵌入定义。虽然 清单 5 只实现了一个协议,即 Fulfillment,但是它后面可以有任何数量的其他协议及其实现。

在下面,defrecord 创建了一个 Java 类,包含 datecustomerproducts 字段。要创建 PurchaseOrder 的新实例,您要用一般的方式来调用其构造函数,为字段提供值,如清单 6 所示:

清单 6. 创建新的 PurchaseOrder 实例
(def po (PurchaseOrder. (System/currentTimeMillis)
                        "Stuart Sierra"
                        ["product1" "product2"]))

一旦构造了实例,您就可以在您的 PurchaseOrder 对象上调用 invoicemanifest 函数,其使用在 defrecord 中提供的实现。但是您还可以像 Clojure 映射那样对待 PurchaseOrder 对象:defrecord 自动添加必需的方法来实现 Clojure 的映射接口。您可以通过名称(作为关键字)检索字段,更新这些字段,甚至添加原始定义中没有的新字段 — 所有都使用 Clojure 的标准映射函数,如以下 REPL 会话所示:

com.amalgamated> (:date po)
1288281709721
com.amalgamated> (assoc po :rush "Extra Speedy")
#:com.amalgamated.PurchaseOrder{:date 1288281709721,
                                :customer "Stuart Sierra",
                                :products ["product1" "product2"],
                                :rush "Extra Speedy"}
com.amalgamated> (type po)
com.amalgamated.PurchaseOrder

请注意在打印记录类型时,它看起来就像前面带有额外类型标签的映射。此类型可通过 Clojure 的 type 函数检索。如果映射和记录类型遵守相同的接口,那么在开发期间尤其方便:您可以开始先使用普通映射,然后在需要额外功能时切换到记录类型,而不会破坏您的代码。

纯理论的行为:deftype

并不是所有的类都表示应用程序域中的结构化数据。其他类型的类通常表示特定于实现的对象,例如集合(例如,ArrayListHashMapSortedSet)或值(例如,StringDateBigDecimal)。这些类实现它们自己的一般接口,因为它们像 Clojure 映射那样没有意义。要填充此角色,Clojure 为那些不是记录的项提供数据类型的变体。虽然 deftype 宏具有与 defrecord 相同的语法,但是它创建基本 Java 类,没有任何类映射功能。类的任何行为都必须作为协议方法实现。您通常使用 deftype 来实现新类型的集合,或定义围绕您所指定协议的全新抽象事物。

一个故意漏掉的数据类型的功能是能够实现没有在任何协议上定义的方法。数据类型必须完全通过其字段和其实现的协议来指定;这样保证在数据类型上调用的函数经永远是多态的。任何数据类型都可被其他数据类型取代,只要它们实现的是相同的协议。


与 Java 代码互动

在过去几个月里,您已悄悄将一些 Clojure 带入 Amalgamated Thingamabobs Incorporated 的生产系统。因为它被编译到 JAR 文件中,所以其他编程团队从未注意过。但是另外一个纯 Java 开发人员团队需要建立另外一个与您的 Clojure 代码兼容的发票生成器。您开始跟他们谈论有关 Clojure、Lisp 和表达式问题,但他们对您说的全然无知。

最后,您说道 “没问题,只要使你们的 Java 类实现此接口就可以了”,同时您向他们展示以下内容:

package com.amalgamated;

public interface Fulfillment {
    public Object invoice();
    public Object manifest();
}

Clojure 已经将您早先定义的协议编译到代表此接口的 JVM 字节码中。实际上,每一个 Clojure 协议还是一个具有相同名称和方法的 Java 接口。如果您谨慎命名(也就是说,在您的方法名称中不使用任何在 Java 语言中无效的字符),则其他 Java 开发人员可以实现该接口,而无需知道它自 Clojure 生成的。

对数据类型也是一样,虽然它们的生成类看上去有一点不像开发人员所希望的那样。在 清单 5 中定义的 PurchaseOrder 数据类型为类生成字节码,如下所示:

public class PurchaseOrder
    implements Fulfillment, 
               java.io.Serializable, 
               java.util.Map,
               java.lang.Iterable,
               clojure.lang.IPersistentMap {

    public final Object date;
    public final Object customer;
    public final Object products;

    public PurchaseOrder(Object date, Object customer, Object products) {
        this.date = date;
        this.customer = customer;
        this.products = products;
    }
}

数据类型的字段被声明为 public final,以与 Clojure 的不变数据策略保持一致。它们只通过构造函数初始化。您可以看到此类实现了 Fulfillment 接口。其他接口都是 Clojure 的数据操作 API 的基础 — 在通过 deftype 而不是 defrecord 创建的数据类型中缺少这些接口。请注意数据类型可实现任何 Java 接口的方法,不仅仅是 Clojure 协议生成的那些接口。

Clojure 具有其他形式的可产生代码的 Java 互操作,这更像惯用的 Java — 但不太像惯用的 Clojure — 在其是唯一的选择时。


结束语

表达式问题在面向对象的编程中是真实的、实际的问题,且针对它的最通用的解决方案是不足的。Clojure 协议和数据类型提供了一种简单、优秀的解决方案。协议允许您在先前存在的类型上定义多态函数。数据类型允许您创建支持先前存在的功能的新类型。结合在一起它们使您可以在两方面上扩展您的代码,如图 3 所示:

图 3. Clojure:轻松添加列(协议)和行(数据类型)
Clojure:轻松添加列(协议)和行(数据类型)

图 3 显示了表的最终版本,改编自 图 1图 2。列表示任何已经存在的函数或方法。行表示已经存在的类 (Java) 和数据类型 (Clojure)。这些行和列的交汇表示了所有先前存在的实现,您不想或无需变更这些实现。您可以添加新的 Clojure 数据类型作为表底部的新行,添加新的 Clojure 协议作为表右侧的新列。此外,您的新协议可扩展到您的新数据类型,并填充新右下角的单元格。

我在这里描述的针对表达式问题的解决方案之外还有一些解决方案。Clojure 本身总是具有多重方法,其提供更加灵活多样的多态性(尽管一个执行的不是很好)。多重方法类似于其他语言中的一般函数,如 Common Lisp。Clojure 的协议还与 Haskell 的类型类 有类似之处。

每一个编程人员或早或晚都将会遇到表达式问题,因此每一种编程语言都会有一些答案。作为一个有趣的体验,可选择任何编程语言 — 您所熟知的一种语言或刚刚学习的语言 — 并考虑您将如何使用它来解决本文中所提供的示例那样的问题。您可以在不修改任何现有代码的情况下执行它吗?您可以在不破坏身份的情况下执行它吗?您可以确保避免名称冲突吗?同时,最重要的是,您可以在未来不创建更大问题的情况下执行它吗?

参考资料

学习

获得产品和技术

讨论

  • 加入 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
ArticleID=647044
ArticleTitle=通过 Clojure 1.2 解决表达式问题
publish-date=04142011