Revised: 淺談OO編程中必不可少的手段:Multimethod(又名Multiple Dispatch、Generic Function)

(此處爲人肉維護的鏡像,不保證完整複製內容到這裏,也不保證及時修訂錯誤。原文請見:http://csbabel.wordpress.com/ 翻牆請用Google Reader查看:http://csbabel.wordpress.com/feed/)

 

這可能是我寫得最中規中矩的文章標題了。寫這個文章的目的很傻很天真:“在網路上能夠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;

(似 乎最近從“癮科技”開始,不少中文站都在說鈔票防僞的事情。我也來PS一槓子:世界上有幾十種鈔票都是打印不了的,因爲有Omron的獵戶座防僞專利技 術,當然,在1996發行那種50馬克之前,就不會有這個問題。具體例子和應用了此技術的鈔幣列表請參見:http://www.ybnotes.com /cn/ennewslist.asp?id=434)

現在我們用簡單的動植物模型來實際編一些程序。基於食物鏈方面的常識,我們可以這樣總結:
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。在這一點上,編程技術也是符合人們的思維習慣的。

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


接下來的文章內容,非常符合作爲讀者的你的預料:我要開始講特例了。一般在特例之後都是將程序搞砸,然後再引入清晰的解決方案來顯示它有多麼好,但是,我不想這樣搞。我只想按照自然的思路來進行。不過,特例還是要引入的:)

話說那天下大事,如果真的是這樣地簡單那該有多好(前面一句請用單田芳的語調來唸)。只是事情往往不在最完美的階段結束,就像好萊塢電影裏說的:And death... is only the beginning...

在一般情況下,上面提到的方式運作良好。但事事有例外。比如我們突然發現有種植物叫豬籠草,它竟然可以喫掉小昆蟲!
1. Animal.Eat(A Plant);
2. Animal.Eat(An Animal);
3. SomePlant.Eat(An Animal); // 喫動物的植物

其實不只是豬籠草這樣的交互特例。前面的例子也省略掉了許多分類方式,比如動物要分爲食草、食肉、雜食的,除此之外有微生物。喫昆蟲的植物其實也進行光合作用……

如果編一張表格,所有的關係就清楚了。


生物
動物
植物
食肉動物
食草動物
雜食動物
微生物
捕食的植物
豬籠草

生物










動物









植物









食肉動物









食草動物









雜食動物









微生物









捕食的植物









豬籠草




















我 目前不打算完成這個表格,因爲這個表格搞起來很麻煩(如果是左小龍的話他肯定也不幹,除非那表格的體溫是37 度:http://www.qidian.com/BookReader/1100289,21986729.aspx)。但即使不完成這個表格,我們也 能知道表格裏會有許多重複項目,比如:如果食草動物能喫所有植物,那麼它就能喫豬籠草(雖然現實生活中它們基本是不喫的)。有了前者作爲規則,後者是不需 要再描述一遍的。一般說來,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、我的大腦
2、大搞學術造假的所有的院士、教授、博士、碩士們寫的所有論文(這樣子能幫他們增加引用不?聽說幫人引用論文還能掙錢呢!)

註釋

[1]關於Concept
Concept在人類的思維和語言交流之中的重要性無論如何誇大也不過份。如果沒有了Concept,我們無法學習,無法描述,無法理解、無法記憶、無法祈使……
在這裏我不打算完整說理,舉幾個例子
例1:請在不使用Concept的情況下,向別人講解圓面積公式(提示:圓、面積、半徑、PI等等都是Concept)
例2:請在不使用Concept的情況下,命令你的小孩放下手中的玩具,坐到這邊的椅子上來(玩具,椅子等都是Concept)
例3:請在不使用Concept的情況下,向你的朋友描述你剛剛是如何從家中來到聚會地點的(汽車、路、交通、堵車、步行等等都是Concept)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章