最后更新于2017年11月6日星期一21:22:35 GMT

如果您查看任何基于oo的非平凡大小的代码库, 您将[希望]发现,通过有效地使用多态性(或者通过接口将调用代码与类型实现解耦),可以很好地形式化和封装易于理解的行为, 或者通过子类型共享多个类型共用的代码.

以静态类型语言(如Java)为例, 让我们来看看Map接口和它在标准库中的一些实现:

一个接收方法,它接受type Map 不需要关心a的不同实现细节 HashMap or TreeMap; it’s enough to rely on the fact that both concrete types support the get() and put(k, v) methods. 如果我们随后想要使用新的Map实现, CoolNewMap,我们可以做到这一点,而无需进行任何代码更改. 像这样跨具体类型的聚合行为在Java / c#世界中很常见:


通过接口上的聚合实现多态性具有 long been preferred 面向对象系统中的过度继承. 但是,如果您想扩展现有具体类型的功能会发生什么呢? 这在像Java这样的语言中是很尴尬的:


而CoolNewMap可以自由地实现Map和ImmutableMap, 接受Map作为参数的接收器不能接受它. 甚至ImmutableMap接口也直接扩展了Map, 如果不将签名缩小到ImmutableMap(或强制转换),我们就无法利用代码中查找Map的额外方法, 但让我们假装我们现在做不到). Additionally, 我们不能让HashMap和TreeMap实现新的接口,因为即使它们是非final的, 我们无法控制代码. 例如,你可能经常遇到这种情况, 假设我们有一个来自供应商的数据库客户端实现 IDBClient:

公有最终类ProprietaryDBClient实现IDBClient {

    @Override
    public void doSomething() {
        //在这里实现
    }
}

即使我们以某种方式完全控制了IDBClient接口, 我们的供应商给了我们一个组件,它:

  1. we can’t change
  2. is marked as final

Hmm. 像这样向密封类型添加功能的常见解决方案是将其装入外部类型:

FancyDBShim实现了SomeNewInterface, IDBClient {

    @Override
    public void doSomething() {
        client.doSomething();
    }

    @Override
    doSomethingNew() {
        //新增功能!
    }
}

装箱有一些问题——如果您委托的接口很大,它会产生大量的样板代码, and more awkwardly, 你失去了原始类型的身份. 由于您现在正在“欺骗”底层DB客户端,因此覆盖包装器。 equals() 方法的行为 ProprietaryDBClient.equals() 可能会造成痛苦的世界,因为他们从根本上 not the same thing.

一些动态语言,比如Ruby,允许“开放”类的概念,只要有必要,就可以修改. 如果您使用过任何一定规模的rails应用程序, you’ll probably see this capability abused for the age-old practice of monkey-patching; if you’re really unlucky you might come across crazy stuff like this:

class String
  def capitalize
    “???”
  end
end

还有两种完全无关的类型:

class A
  def print_msg
    puts “a”
  end
end

class B
  def print_msg
    puts “b”
  end
end

但是只要它们都响应print_msg的调用,那么这样做是完全有效的. 因为像Java这样的静态类型语言在编译时解析调用位置, 我们没有这种奢侈. 值得注意的是,动态类型并不能神奇地使不显眼地将新功能聚合到现有类型上变得更容易——Ruby允许您这样做的唯一方法是通过重新打开类的子类型多态性,如前所述.

这就引出了一个问题——是否有一种方法可以在保持这些不变量的同时为现有类型添加新功能:

  • The code 因为原始代码没有改动
  • The original type’s identity is left untouched
  • We don’t extend the original type

多年来,这一系列限制一直在挑战语言设计师,但Philip Wadler在90年代末将这一挑战描述为“表达问题”. 他是这样说的:“目标是按案例定义数据类型, 在哪里可以向数据类型添加新用例,并在数据类型上添加新函数, 无需重新编译现有代码, 在保持静态类型安全的同时.
The adding-new-cases 对于Java/ c#程序员来说,bit应该很熟悉——这就是我们垂直扩展表的地方. The new-functions 动态/函数式程序员应该很熟悉——这就是我们横向扩展表的地方. 一种可以做到的语言 both 解决了表达式问题.
Clojure对表达式问题的解决方案使用的概念是 protocols 以类似于接口的方式制定规范,同时还允许您扩展现有类型,而无需任何装箱或重新编译. 再次以我们笨拙的DB客户端为例,我们可以这样声明一个协议:

(defprotocol com.example.(doSomethingNew [this]))

Since Clojure is not an object-oriented language it has no concept of instance state; instead function definitions take an explicit ‘self’ parameter as the first argument, called this by convention. 此时,我们的somenewfunctional接口看起来非常类似于等效的Java接口, 实际上,在JVM上运行时,它将生成一个真正的接口,称为 com.example.SomeNewFunctionality. 不像普通的接口,不能直接连接到 ProprietaryDBClient 类,我们可以用我们的协议扩展它:

(extend com.dbvendor.ProprietaryDBClient
  SomeNewFunctionality
  {:doSomethingNew #(println %)})

如果你不熟悉Clojure, #(…) 是该语言中除了宏系统之外的少数语法之一:它是定义匿名函数的简写,其中%是第一个也是唯一的参数-在这种情况下, 我们的实现只是打印对象, 隐式调用Java的 toString() 方法在ProprietaryDBClient实例上. 我们可以确认任何新创建的实例都符合ProprietaryDBClient和somenewfunction类型:

(def instance (…) ; get a DB client instance from somewhere
(isa? ProprietaryDBClient instance) ; => true
(isa? SomeNewFunctionality instance) ; => true

我们还可以在实例上调用来自somenew功能性的新功能, 就像其他函数一样:

(doSomethingNew实例)

Protocols, therefore, 解决接口或子类型多态性本身无法解决的“逐案”函数定义需求. 事实上,一个纯Java解决方案在几年前就被创造出来了(paper here), 尽管依赖于接口继承和泛型远不如像协议这样的东西具有表现力,而协议是从头开始设计来解决这个问题的. 最重要的是,我并不是要赞美一种语言比另一种语言的优点,但希望从这里你可以看到语言设计师必须面对的一些挑战(有时也会被诋毁)!)以及定义明确的重要性, 清晰分离和可扩展的接口, 不管你用的是什么编程语言.


准备好开始从您的应用程序中获得见解? Sign up for a Logentries免费试用 today.