Jim Gay July 6, 2012

四人帮(Gang of Four)错了。 Ruby 的标准库也错了。 Rails 也错了。

如果每个人都这么做,它还是错的吗?

是的。

四人帮的书籍 Design Patterns 为我们提供了通用的词汇来理解面向对象编程中的基本模式。 它帮助我们所有人用相同的术语来讨论我们的软件,并防止混淆。

可悲的是,这本书似乎引起了大量的混乱。

他们说“优先选择组合而非继承”。 这很好,这样做有很好的理由。

他们说要使用 "Delegation"。 这很好。 虽然他们的书中没有一个 Delegation 的例子。

I do not think it means what you think it means Delegation 是一种常被引用的技术,作为在程序中创建灵活性的方式。 也就是说,Delegation 经常被作为组合的方式。

但是 Delegation 并不是你想象的那样。 四人帮把你引入了歧途。

更糟糕的是,几乎你找到的每一个关于 "Delegation" 的参考资料,都向你展示了对象协作和消息转发的例子。 它们只是普通的方法调用,而不是 Delegation 的例子。

现在想象一下你的导师会如何评价你对基本编程概念的理解...

没有导师? 没关系。 想象一下你有。 现在想象一下你的导师会如何评价你对基本编程概念的理解...

你应该正确地理解它们,对吧?

那么什么是 Delegation?

Delegation 很容易理解,但我们太习惯于听到和阅读它,并将其与别的东西联系起来。 让我们来解决这个问题。

Henry Lieberman 在 Using prototypical objects to implement shared behavior in object-oriented systems 中创造了 "Delegation" 这个术语,该论文与 OOPLSA '86 会议关于面向对象编程系统、语言和应用的会议论文集一起发布。 该内容需要付费才能访问,但你可以在 他的网站 上阅读类似和更新的内容

在那里,你会准确地找到什么是 Delegation,但我不会让你离开去研究另一篇长篇文章; 我可以为你简化它。 但是,当你有时间时,请阅读 Lieberman 自己的话。

Lieberman 在 GUI 绘图工具方面讨论了这一点。 以下是 Delegation 背后的关键思想:

当一支笔将 draw 消息 Delegation 给一个原型笔时,它是在说 "我不知道如何处理 draw 消息。 我希望你帮我回答它,如果你可以,但是如果你有任何进一步的问题,比如我的 x 变量的值是多少,或者需要做任何事情,你应该回到我身边并询问。" 如果消息被进一步 Delegation,那么关于变量值的任何问题或回复消息的请求都将被推断到最初 Delegation 消息的对象。

简而言之,当你向一个对象发送消息时,它有一个 "self" 的概念,它可以在其中找到属性和其他方法。 当该对象 Delegation 给另一个对象时,对 "self" 的任何引用始终指的是原始消息接收者。 总是。

另一种继承

基于类的继承不是唯一的选择。 基于原型的继承是另一种构建对象以进行行为共享的方式。 一种方法是在抽象位置(类)中设置行为,另一种方法是在实例(对象)中设置行为。

Self 是一种编程语言,它实现了 Lieberman 的定义。 Self 具有包含 slots 的对象。 每个 slot 可以包含一个方法,或者一个对 parent slot 中原型对象的引用。 如果一个对象收到它不理解的消息,它可以 Delegation 给它的 parent slot 中的一个对象。

这是原型继承,而不是类继承。 它类似于,但不等同于,说一个对象有一个类(比如 Ruby 对象),其中包含额外的行为。

Self 给我们提供了一个带有 parent slot 的对象,这相当于 JS 给我们提供了一个带有 prototype 引用的对象。

JS 是最流行的基于原型的语言,而且更容易尝试,所以与其教你 Self,不如看看 JS。 你甚至可以打开浏览器的控制台并立即尝试。

在 JS 中,我们可以分配一个 prototype:相当于 Self 中的 parent slot。

function Container(){};
Container.prototype = new Object();
Container.prototype.announce = function(){ alert("these are my things: " + this.things) };
function Bucket(things){this.things = things};
Bucket.prototype = new Container();
bucket = new Bucket("planes, trains, and automobiles")
bucket.announce() // alerts "these are my things: planes, trains, and automobiles"

在 Delegation 的说法中,bucket 对象是 client,它将消息转发给 delegate

在上面的例子中,你可以看到 this.things 的评估是在 client 对象的上下文中完成的。 当我们调用 announce 函数时,它是在 delegate 对象上找到的。 当函数被评估时,this 指的是 client。

当 JS 对象的 prototype 包含一个函数时,该函数被评估,就好像该对象具有该方法一样。 第一个例子表明 this (在 JS 中就是 self)总是指的是原始消息接收者。

Ruby 呢?

首先,理解转发(forwarding)。 转发是将消息从一个对象传递到另一个对象。 Ruby 标准库 forwardable 的命名非常贴切,它允许你将消息从一个对象转发到另一个对象。

现在,让我们看看 Ruby 标准库中命名不佳的 delegate 库,它也允许你将消息从一个对象转发到另一个对象。

require 'delegate'
# assuming we have a Person class with a name method
person = Person.new(:name => 'Jim')
class Greeter < SimpleDelegator
 def hello
  "Hi! I'm #{name}."
 end
end
greeter = Greeter.new(person)
greeter.hello #=> "Hi! I'm Jim."

Greeter 内部发生的事情是,当 greeter 实例被初始化时,它包含对 person 的引用。 当调用一个未知方法时,它被 forwarded 到目标对象(也就是 person)。 记住,我们仍然在看 delegate 库,它帮助我们转发消息... 困惑吗? 是的,我也是。 所以,似乎世界上的其他地方也是如此。

转发只是将消息传递给一个对象:一个方法调用。

这与 Delegation 的区别在于,这些库允许你轻松地将消息转发到其他对象,而不是像我们在 JS 中看到的基于原型的继承那样,在第一个对象的上下文中执行另一个对象的方法。

我们通常几乎没有意识到这一点,因为 Ruby 的 method_missing 功能在 SimpleDelegator 中发挥了作用。 我们认为方法会自动转到我们想要的对象。

即使我们的 Greeter 方法引用了 self,该消息也会被转发到另一个对象并在那里处理,当然,前提是 client 上缺少该方法。

如果我们想共享行为而不使用额外的方法扩展对象,那么 method_missing 和/或 SimpleDelegator 可以很好地完成这项工作。 这适用于简单的用途。 但是,它会破坏对对象类的引用。

例如,假设我们想用某种新的问候语来引用 client 对象的类。 我们不想完全重写该方法,因此我们将依靠 super 来获取在常规 Greeter 中实现的任何内容。

class ProperGreeter < Greeter
 def name
  "the esteemed " + self.class.name + ", " + super
 end
end
proper_greeter = ProperGreeter.new(person)
proper_greeter.hello #=> "Hi! I'm the esteemed ProperGreeter, Jim."

嗯。 这不是我们所期望的。 我们想看到的是 "the esteemed Person"。

这简单地说明了我们有 两个 对象,并且是面向对象设计中的 self-schizophrenia(自我分裂) 的一个例子。 每个对象都有自己的身份和 self 的概念。 我们可以通过更改引用以使用 __getobj__(SimpleDelegator 期望的方式)而不是 self 来解决这个问题,但这说明了 self 并不指我们想要的东西。 我们必须显式地使用两个对象,并且我们的心理模型始终需要我们考虑两个对象之间的交互,而我们实际上只关心改变一个对象的行为。

这不是 Delegation。 这始终是两个对象之间的协作。 尽管无数的书籍、文章和软件库告诉你相反。 四人帮把 Delegation 搞错了,但这没关系,因为现在你知道得更清楚了。

随便,我想怎么称呼它就怎么称呼

谁在乎。 让我们不要兴风作浪。 每个人都在这样做。

是的,《Design Patterns》展示了 C++ 中的例子。 而 C++ 无法做到 Delegation。 那么这是否是重写术语含义的有效论据?

如果你的语言无法提供它,不要只是把你语言 可以 做的事情称为 "Delegation"。

掌握正确的概念

开发应用程序需要你不仅要理解你的业务领域概念,还要理解许多工具和方法来设计。 软件设计模式是解决常见问题的常用方法。 了解它们是什么以及何时使用它们是成为高效开发人员的关键部分。

《Design Patterns》一书经常因将名称赋予常用软件设计模式而受到赞扬。 这有助于我们所有人用相同的术语来讨论我们的软件,并防止混淆。

当然,当我就 Clean Ruby 开始研究时,我拿起《Design Patterns》这本书来获得一些清晰的认识。 我想,“当然,这本书将有助于指导对如何处理不同模式的讨论和理解”。 可悲的是,它包含一个明显的错误,这个错误已经在无数的书籍、文章和软件库中被重复和编纂。 我一遍又一遍地阅读了对软件中行为共享和对象组合模式的相同描述,但几乎总是给出了错误的名称。

《Design Patterns》一书对于推动 “组合优于继承” 的思想非常有用。 事实上,似乎有如此多的开发人员喜欢这个短语,以至于下次你争论建立基于类的继承时,你可能会被这个术语击中。 值得庆幸的是,Sandi Metz 在 Practical Object Oriented Design in Ruby 中捍卫了基于类的继承的逻辑目的。

《Design Patterns》经常受到赞扬,不是因为它具有创新性,而是因为它对术语进行了整合。 它指导我们使用语言来理解和讨论处理不同问题的方式。 它为我们命名了常见的模式,以便我们可以更好地相互交流我们正在创造的东西。

但是,当你尝试理解 Delegation 时,这种赞扬就有点苍白了。

我的书 Clean Ruby 深入探讨了对 Delegation 的理解,并探索了保持项目井然有序、可维护和松散耦合的方法,为你提供了共享行为和避免认知开销的方法。 立即获取它,并理清你的概念。

Newer Post $15,000 in Income From an eBook, How I Did It Older Post How to preserve idioms in Ruby subclassing Back to Top