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

如果您查看任何基于oo的非平凡大小的代码库, you’ll [hopefully] find well understood behavior formalized and encapsulated through the effective use of polymorphism- either via interfaces which decouple calling code from a types’ implementation, 或者通过子类型共享多个类型共用的代码.

以静态类型语言(如Java)为例, let’s look at the Map interface and a few of its implementations in the standard library:

一个接收方法,它接受type Map doesn’t have to concern itself with the different implementation details of 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,我们可以做到这一点,而无需进行任何代码更改. Aggregating behavior across concrete types like this is commonplace in the Java / C# world:


通过接口上的聚合实现多态性具有 long been preferred 面向对象系统中的过度继承. However, what happens if you want to extend the capabilities of an existing concrete type? 这在像Java这样的语言中是很尴尬的:


而CoolNewMap可以自由地实现Map和ImmutableMap, 接受Map作为参数的接收器不能接受它. 甚至ImmutableMap接口也直接扩展了Map, we can’t leverage the extra methods in code that looks for Map without narrowing the signature to ImmutableMap (or casting, 但让我们假装我们现在做不到). Additionally, we can’t make HashMap and TreeMap implement the new interface because even though they’re non-final, 我们无法控制代码. 例如,你可能经常遇到这种情况, 假设我们有一个来自供应商的数据库客户端实现 IDBClient:

公有最终类ProprietaryDBClient实现IDBClient {

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

Even if we somehow have full control over the IDBClient interface, 我们的供应商给了我们一个组件,它:

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

Hmm. A common workaround for adding functionality to a sealed type like this is to box it in an outer one:

public class FancyDBShim implements SomeNewInterface, IDBClient {

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

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

Boxing has a few problems- it incurs a lot of boilerplate code if the interface you’re delegating is large, and more awkwardly, 你失去了原始类型的身份. Since you’re now ‘spoofing’ the underlying DB client, overriding the wrappers’ equals() 方法的行为 ProprietaryDBClient.equals() is probably going to cause a world of pain because they’re fundamentally not the same thing.

Some dynamic languages such as Ruby allow for the notion of ‘open’ classes which can be changed whenever it’s deemed necessary. 如果您使用过任何一定规模的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

But as long as they both respond to invocations of print_msg, it’s perfectly valid to do this. Since statically typed languages like Java resolve call sites at compile time, 我们没有这种奢侈. It’s important to note though that dynamic typing doesn’t magically make it any easier to unobtrusively aggregate new functionality onto existing types- the only way Ruby lets you do this is through subtype polymorphism through reopening classes as detailed previously.

This begs the question- is there a way we can add new functionality to existing types while maintaining these invariants:

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

This set of constraints has challenged language designers for years but Philip Wadler characterized the challenge as the ‘expression problem’ in the late 90s. 他是这样说的:“目标是按案例定义数据类型, where one can add new cases to the datatype and new functions over the datatype, 无需重新编译现有代码, 在保持静态类型安全的同时.
The adding-new-cases bit should be familiar to Java/C# programmers- that’s where we extend our table vertically. The new-functions bit should be familiar to dynamic/functional programmers- that’s where we extend our table horizontally. 一种可以做到的语言 both 解决了表达式问题.
Clojure’s solution to the expression problem uses the concept of protocols to lay down specifications in a similar way to interfaces while also allowing you to extend existing types without any boxing or recompilation. Taking the example of our awkward DB client again, we might declare a protocol like this:

(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. At this point our SomeNewFunctionality interface looks pretty similar to an equivalent Java interface, and in fact when running on the JVM it will generate a real interface called com.example.SomeNewFunctionality. 不像普通的接口,不能直接连接到 ProprietaryDBClient 类,我们可以用我们的协议扩展它:

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

如果你不熟悉Clojure, #(…) is one of the few bits of syntax in the language aside from the macro system: it’s a shorthand for defining an anonymous function where % is the first and only argument- in this case, 我们的实现只是打印对象, 隐式调用Java的 toString() 方法在ProprietaryDBClient实例上. We can confirm that any newly created instances both conform to the types ProprietaryDBClient and SomeNewFunctionality:

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

We can also invoke new functionality from SomeNewFunctionality on instance, 就像其他函数一样:

(doSomethingNew实例)

Protocols, therefore, solve the ‘case-by-case’ function definition requirement that interfaces or subtype polymorphism on their own cannot. It actually turns out that a pure Java solution was coined a few years ago (paper here), although relying on interface inheritance and generics is nowhere near as expressive as something like protocols which were designed from the ground-up to solve this problem. Above all I’m not trying to extol the virtues of one language over another- but hopefully from this you can see some of the challenges language designers have to face (and are sometimes vilified for!)以及定义明确的重要性, 清晰分离和可扩展的接口, 不管你用的是什么编程语言.


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