Revised: 浅谈OO编程中必不可少的手段:Multimethod(又名Multiple Dispatch、Generic Function) .

“在網路上能夠google到的介紹multimethod的漢語文章很少”。有本很出名的书写了一笔(More Effective C++: Item 31),但很快就转而使用替代方案了。)

一点历史介绍

在上世纪的80年代中期,在LISP中出现了CLOS(读成see-loss)系统,它是Common Lisp Object System的缩写。CLOS被提出之后不久正好赶上了ANSI标准的制定。CLOS是如此之优秀,以至于它一字不改就被收录成了ANSI标准。因为它是 ANSI标准的一部分,所以基本上所有的Common LISP都带有CLOS。CLOS提供了很强大的功能,包括class定义,子类化(派生/继承),multimethod,before/after /around,重定义class,改变object的class,重新初始化object等功能(除此之外,CLOS还提供了一个强大的Meta- Object协议)。所有的这些,从功能上超过了任何其它支持OO编程的语言(就如同LISP的Condition超过了其它语言的Exception一样)。

Multimethod是在设计CLOS的过程中提出来的。因为设计者们发现,不加入Multimethod的话,OO编程实践中就缺少一个必要的手段。从这个角度来说,虽然Multimethod只能算CLOS中的一碟小菜,但它却是一碟很多人都需要、但是C++和JAVA就是不给上——为什么要这么说呢?且慢慢听我道来……

很自然很舒服的面向对象

在多数教科书和随笔文章中,当讲解到面向对象技术时,一般都会举出动物的例子。我临时也编出来一个:
生物分为动物和植物;而动物可分为两栖、爬行和哺乳动物。哺乳动物中有狗有狼还有人,哺乳动物都有一个共同的行为:哺乳;两栖动物都有一个共同的属性:变态。

这个例子能很好的说明OO技术的自然性——它和人类认知中的concept正好对应[注1]:
1、Concept是一个分类方法;
2、Concept有它不同于其它concept的特有属性;
3、同属某个concept的个体有同样的属性;
4、Concept可以有层级关系,子concept拥有父concept的所有特性(但不一定能当成父concept来用,参考Liskov Subst Principle);

从这个角度来说,OO是成功的,因为它用 很自然地,符合一般人思维的方式解决了一个复杂的归类问题。(这里暂且不提interface这回事,其实真正的OO是interface的天下,但interface却不是那么“自然”的)

如果我们记住了一个概念,我们就能够了解它所代表的事物的特点和功能,从而能够很好地使用它。OO技术带来了同样强大的(复用)能力——只要知道了class名字,就可以知道object的属性和功能,并方便地使用它们。

上面的例子可以总结成这样的关系:

类别间的关系

  • 植物 is a 生物
  • 动物 is a 生物
  • 两栖动物 is a 动物
  • 爬行动物 is a 动物
  • 哺乳动物 is a 动物
  • 狼 is a 哺乳动物
  • 狗 is a 哺乳动物
  • 人 is a 哺乳动物

行为

  • 两栖动物 can 变态
  • 哺乳动物 can 哺乳


(狼、狗、人的“哺乳”行为可以从“哺乳动物”中继承而来,不需要再进行重复声明)

分类和交互:复杂性之源


世界之所以复杂,不仅仅在于事物的多样性(分类),还在于事物之间的交互。一般意义上的交互是简单的,比如下面这段程序伪码(本想用汉编的,无奈我太笨,学不会,只好用英编了)

对于打印机打印图片,我们可以这样写
function Printer.Print(RMB rmb) Action:
1. Render rmb to memory;
2. Send memory to PrinterDriver;
3. Submit to PrinterDriver
End Action;

现在我们用简单的动植物模型来实际编一些程序。基于食物链方面的常识,我们可以这样总结:
1. Animal.Eat(A Plant);
2. Animal.Eat(An Animal);

于是,我们这样定义动物类型,添加一个方法,名叫“吃”:
class Animal;
class Plant;

function Animal.Eat(Animal victim) Action:
me.Energy += victim.Energy;
End Action;

function Animal.Eat(Plant food) Action:
me.Energy += food.Energy;
End Action;

如果我们定义了猿是动物的子类别:
class Ape derived from class Animal;
那么Ape同样可以去Eat所有的Animal和Plant(child class从super class继承而来的)

你可能注意到了,对于同一个类型“动物”,我们定义了两个同样的方法,都名为“吃”,但是有不同的参数。是的,我们可以这样编程,这东西有个很响亮的名称叫重载overload。在这一点上,编程技术也是符合人们的思维习惯的。

啊——喔——出现了特例!


接下来的文章内容,非常符合作为读者的你的预料:我要开始讲特例了。一般在特例之后都是将程序搞砸,然后再引入清晰的解决方案来显示它有多么好,但是,我不想这样搞。我只想按照自然的思路来进行。不过,特例还是要引入的:)

在一般情况下,上面提到的方式运作良好。但事事有例外。比如我们突然发现有种植物叫猪笼草,它竟然可以吃掉小昆虫!
1. Animal.Eat(A Plant);
2. Animal.Eat(An Animal);
3. SomePlant.Eat(An Animal); // 吃动物的植物

其实不只是猪笼草这样的交互特例。前面的例子也省略掉了许多分类方式,比如动物要分为食草、食肉、杂食的,除此之外有微生物。吃昆虫的植物其实也进行光合作用……

如果编一张表格,所有的关系就清楚了。


生物
动物
植物
食肉动物
食草动物
杂食动物
微生物
捕食的植物
猪笼草

生物










动物









植物









食肉动物









食草动物









杂食动物









微生物









捕食的植物









猪笼草




















但即使不完成这个表格,我们也能知道表格里会有许多重复项目,比如:如果食草动物能吃所有植物,那么它就能吃猪笼草(虽然现实生活中它们基本是不吃的)。有了前者作为规则,后者是不需要再描述一遍的。一般说来,super class的规则基本上适用child class,但是child class有时候会有特殊规则。如果能够识别child class,对于特殊的child class按照特殊规则处理,没有特殊规则的就按super class的规则进行处理,这样就比较完美了。

用Multimethod编写交互规则


可能很多人都同意这一条:变化是重构的ringing bell。考虑到植物动物和微生物的分类关系,与它们之间的“吃”与“被吃”的复杂关系。很明显地,重构的结果应该能够表达上面的这个表格中的内容。但手工编码所有的这些对应关系肯定是last way to go(参见本文的last paragraph)。如果能有一种书写方式将一般规则和特殊规则结合起来——child class拥有特殊规则时就应用特殊规则,否则就应用针对super class书写的一般规则——如果能这样书写就同样自然、同样舒服了:

这种书写方式就是Multimethod:

一上来还是class的定义:
class Organism; // 定义生物
class Animal derived from class Organism;   // 定义动物类型
class Plant derived from class Organism;    // 定义植物类型
class Microbe derived from class Organism;  // 定义微生物类型
class Insect derived from class Animal;     // 定义动物类型
class PredaciousPlant derived from class Plant;  // 定义捕食的植物类型
class Nepenthes derived from class PredaciousPlant; // 定义猪笼草类型
class Herbivores derived from class Animal; // 定义食草动物
class Carnivore derived from class Animal;  // 定义食肉动物
class Omnivore derived from class Carnivore and class Herbivores;   // 定义杂食动物
class Ape derived from class Omnivore;      // 定义猿

然后是Multimethod方式编写的交互规则:

// 生物互吃:暂时不知道如何定义
function Eat(Organism predator, Organism victim) Action:
Print "Don't known how to eat";
End Action;

// 草食动物吃植物(包括杂食动物)
function Eat(Herbivores predator, Plant victim);

// 肉食动物吃动物(包括杂食动物)
function Eat(Carnivore predator, Animal victim);

// 捕食的植物号昆虫
function Eat(PredaciousPlant predator, Insect victim);

// 猪笼草吃昆虫
function Eat(Nepenthes predator, Insect victim) Action:
Print "Any special processes for nepenthes";
End Action;

// 草食植物吃磨菇(包括杂食动物)
function Eat(Herbivores predator, Microbe victim) Action:
If victim.IsMushroomLike then ...;
End Action;


// 微生物分解死去的动物
function Eat(Microbe predator, Animal victim) Action:

if victim.IsDead then call decompose(victim);
End Action;


// 微生物分解死去的植物
function Eat(Microbe predator, Plant victim) Action:
if victim.IsDead then call decompose(victim);
End Action;


很简单、很自然,很符合人类思维。

熟悉OO的可能会发现,这写法看起来很像重载,但实际上……它是一个电吹风运行时的机制。Multimethod可以按参数的实际类型决定调用哪个函数,也就是说,你可以按Organism类型传入参数,但它实际是一个吹风机猪笼草对象。在运行时,就会首先用猪笼草类型去匹配,以便决定使用哪个函数。
Multimethod与重载(overload)是有明显的区别的。重载是编译时的机制,在运行时不会起作用:如果你以Organism类型传入两个参数,不管实际上它是什么样的child class object,它只会去调用Eat(Organism, Organism),而不会去尝试区别实际child class。

在LISP中测试Multimethod

学习翠花,直接上代码。因为这些代码的自我说明能力很强,就不多加说明了。

(defclass organism() ()) ;;; 定义生物类型
(defclass animal (organism) ())   ;;; 定义动物类型
(defclass plant (organism) ())     ;;; 定义植物类型
(defclass microbe (organism) ())   ;;; 定义微生物类型
(defclass insect (animal) ())      ;;; 定义动物类型
(defclass predacious-plant (plant) ())   ;;; 定义捕食的植物类型
(defclass nepenthes (predacious-plant) ()) ;;; 定义猪笼草类型
(defclass herbivores (animal) ())  ;;; 定义食草动物
(defclass carnivore (animal) ())   ;;; 定义食肉动物
(defclass omnivore (carnivore herbivores) ())    ;;; 定义杂食动物
(defclass ape (omnivore) ())       ;;; 定义猿

;;; 声明Multimethod函数
(defgeneric eat(predator victim)
  (:documentation "Something eats another"))

;;; 生物互吃:暂时不知道如何定义
(defmethod eat((predator organism) (victim organism))
  (format t "Organism --> Organism: Don't known how to eat..."))

;;; 动物吃植物:只有某些动物吃植物
(defmethod eat((predator animal) (victim plant))
  (format t "Only some of animals eat plant..."))

;;; 昆虫吃植物:多数昆虫都是吃植物的
(defmethod eat((predator insect) (victim plant))
  (format t "Most of the insects eat plant..."))

;;; 草食动物吃植物(包括杂食动物)
(defmethod eat((predator herbivores) (victim plant))
  (format t "Herbivores --> Plant"))

;;; 肉食动物吃动物(包括杂食动物)
(defmethod eat((predator carnivore) (victim animal))
  (format t "Carnivore --> Animal"))

;;; 捕食的植物号昆虫
(defmethod eat((predator predacious-plant) (victim insect))
  (format t "Predacious Plant --> Insect"))

;;; 猪笼草吃昆虫
(defmethod eat((predator nepenthes) (victim insect))
  (format t "Nepenthes --> Insect"))

;;; 草食植物吃磨菇(包括杂食动物)
(defmethod eat((predator herbivores) (victim microbe))
  (format t "Herbivores --> Mushroom (Microbe)"))

;;; 微生物分解死去的动物
(defmethod eat((predator microbe) (victim animal))
  (format t "Deposing dead animal (Microbe --> Animal)"))

;;; 微生物分解死去的植物
(defmethod eat((predator microbe) (victim plant))
  (format t "Deposing dead plant (Microbe --> Plant)"))

下面是对eat函数进行各种测试的结果:

LISP> (eat (make-instance 'organism) (make-instance 'organism))
Organism --> Organism: Don't known how to eat...

LISP> (eat (make-instance 'herbivores) (make-instance 'organism))
Organism --> Organism: Don't known how to eat...

LISP> (eat (make-instance 'ape) (make-instance 'organism))
Organism --> Organism: Don't known how to eat...

LISP> (eat (make-instance 'ape) (make-instance 'animal))
Carnivore --> Animal

LISP> (eat (make-instance 'herbivores) (make-instance 'plant))
Herbivores --> Plant

LISP> (eat (make-instance 'ape) (make-instance 'plant))
Herbivores --> Plant

LISP> (eat (make-instance 'ape) (make-instance 'ape))
Carnivore --> Animal

LISP> (eat (make-instance 'insect) (make-instance 'animal))
Organism --> Organism: Don't known how to eat...

LISP> (eat (make-instance 'insect) (make-instance 'ape))
Organism --> Organism: Don't known how to eat...

LISP> (eat (make-instance 'insect) (make-instance 'nepenthes))
Most of the insects eat plant...

LISP> (eat (make-instance 'nepenthes) (make-instance 'animal))
Organism --> Organism: Don't known how to eat...

LISP> (eat (make-instance 'nepenthes) (make-instance 'insect))
Nepenthes --> Insect

LISP> _

在C++中实现Multimethod

在C++中,因为只有重载,而没有multimethod,为了达到同样的效果,你需要这样写:
void Eat(
Organism predator, Organism victim)
{
  if ( predator
is_a
Herbivores )
  {
    if ( victim is_a Plant )
    {
      ...
    }
    else if ( victim
is_a
Microbe && victim.IsMushroomLike() )
    {
      ...
    }
  }
  else if ( predator
is_a
... )
  ...
}
注:上面的is_a是一个伪操作符,用来判断前者的实际类型可不可以是后者(请联想instanceof)。

这种方法称为BF法,特点是“霸王硬上弓”,一切全靠if-else和cast。维护这样的一个大型if-else嵌套结构可不是一件容易的事情。当然,如果使用Visitor模式,肯定是可以解决这样的问题。但那样会导致问题的表达绕了很大的圈子。不直接表达就导致难于理解,而且书写起来很麻烦,扩展/修改起来也很费力。最致命的一点,它只能处理double-dispatch问题,也就是只有两个继承体系的交互问题,对于三个或三个以上的,就无能为力了。Multimethod则可以处理多个参数,也就是多个继承体系之间的交互问题,在参数列表中添加一个参数就可以了。

(还有一位被肠子捆住脖子的邪神LOKI,它有一种用模板来实现dispatch的方法,因为很复杂,也不太好用,这里就不再详述了,感兴趣的可以在google猛搜LOKI和独眼龙奥丁的故事)

注释

[1]关于Concept
Concept在人类的思维和语言交流之中的重要性无论如何夸大也不过份。如果没有了Concept,我们无法学习,无法描述,无法理解、无法记忆、无法祈使……
在这里我不打算完整说理,举几个例子
例1:请在不使用Concept的情况下,向别人讲解圆面积公式(提示:圆、面积、半径、PI等等都是Concept)
例2:请在不使用Concept的情况下,命令你的小孩放下手中的玩具,坐到这边的椅子上来(玩具,椅子等都是Concept)
例3:请在不使用Concept的情况下,向你的朋友描述你刚刚是如何从家中来到聚会地点的(汽车、路、交通、堵车、步行等等都是Concept)

你可能感兴趣的:(Revised: 浅谈OO编程中必不可少的手段:Multimethod(又名Multiple Dispatch、Generic Function) .)