來(lái)源 :CSDN知識庫
幾十年來(lái)我都在用面向對象的語(yǔ)言編程。我用過(guò)的第 一個(gè)面向對象的語(yǔ)言是 C++,后來(lái)是 Smalltalk,是 .NET 和 Java。 我曾經(jīng)對使用繼承、封裝和多態(tài)充滿(mǎn)熱情。它們是范式的三大支柱。 我渴望實(shí)現重用之美,并在這個(gè)令人興奮的新天地中享受前輩們積累的智慧。 想到將現實(shí)世界的一切映射到類(lèi)中,使得整個(gè)世界都可以得到整齊的規劃,我無(wú)法抑制自己的興奮。 然而我大錯特錯了。
01 繼承,倒塌的第 一根支柱
乍一看,繼承似乎是面向對象范式的zui大優(yōu)勢。所有新手教程講解繼承時(shí)都會(huì )拿出zui簡(jiǎn)單的繼承的例子,而這個(gè)例子似乎很符合邏輯。

甚至以后的一切都是重用了。 我囫圇吞下這一切,然后帶著(zhù)新發(fā)現興沖沖地奔向世界了。 香蕉猴子叢林問(wèn)題 帶著(zhù)滿(mǎn)腔的信仰和解決問(wèn)題的熱情,我開(kāi)始構建類(lèi)的層次結構然后寫(xiě)代碼。似乎一切皆在掌控中。 我永遠不會(huì )忘記我準備從已有的類(lèi)繼承并實(shí)現重用的那一 天。那是我期待已久的時(shí)刻。 后來(lái)有了新的項目,我想起了另一個(gè)項目里我很喜歡的那個(gè)類(lèi)。 沒(méi)問(wèn)題,重用拯救一切。我只需要把那個(gè)類(lèi)拿過(guò)來(lái)用就好了。 嗯……其實(shí)……不僅是那一個(gè)類(lèi)。還得把父類(lèi)也拿過(guò)來(lái)。但……應該就可以了吧。 額……不對,似乎還需要父類(lèi)的父類(lèi)……還有……嗯,我們需要所有的祖先類(lèi)。好吧好吧……搞定了。沒(méi)問(wèn)題。 不錯。但編譯不過(guò),怎么回事?哦我知道了……這個(gè)對象還需要另一個(gè)對象。所以那個(gè)也得拿過(guò)來(lái)。沒(méi)問(wèn)題…… 等等……我不僅需要那個(gè)對象,還需要那個(gè)對象的父類(lèi),和父類(lèi)的父類(lèi),和……包含的所有對象的所有祖先…… 唉…… Erlang 的創(chuàng )建者 JoeArmstrong 有句名言:
面向對象語(yǔ)言的問(wèn)題在于,它們依賴(lài)于特定的環(huán)境。你想要個(gè)香蕉,但拿到的卻是拿著(zhù)香蕉的猩猩,乃至zui后你擁有了整片叢林。
香蕉猴子叢林的解決方法 這個(gè)問(wèn)題的解決方法是,不要把類(lèi)層次建得那么深。但如果繼承是重用的關(guān)鍵,那么給繼承機制添加的任何限制都會(huì )限制重用。對吧? 沒(méi)錯。 那我們可憐的面向對象程序員該怎么辦?指望一杯三聚氰胺奶維系我們的健康嗎? 答案就是:包含和委托(Contain and Delegate)。一會(huì )兒會(huì )詳細解釋。 菱形繼承問(wèn)題 早晚你會(huì )遇到下面這種惡心的問(wèn)題,有些語(yǔ)言甚至根本解決不了。

注意 Scanner 和 Printer 類(lèi)都實(shí)現了名為 start 方法。 那么問(wèn)題來(lái)了,Copier繼承哪個(gè)start?是Scanner的還是Printer的?肯定不可能同時(shí)繼承啊。 菱形繼承的解決 解決方案很簡(jiǎn)單:不要這樣做。沒(méi)錯。大多數面向對象都不讓你這么干。 但是,但是……要是必須這樣建模該怎么辦?我需要重用! 那就必須使用包含和委托。

注意現在 Copier 類(lèi)包含一個(gè) Printer 實(shí)例和一個(gè) Scanner 實(shí)例。然后將 start 函數委托給 Printer 類(lèi)的實(shí)現。要委托給 Scanner 也很簡(jiǎn)單。 這個(gè)問(wèn)題是繼承這根支柱上的另一條裂縫。 脆弱的基類(lèi)問(wèn)題好吧,那我盡量使用較淺的類(lèi)層次結構,并保證里面沒(méi)有環(huán),這樣就不會(huì )出現菱形繼承了。 似乎一切都解決了。直到我們發(fā)現…… 我前一 天工作得好好的代碼今天出錯了!關(guān)鍵是,我沒(méi)有改任何代碼! 嗯也許是個(gè) bug……但等等……的確有些改動(dòng)…… 但改動(dòng)的不是我的代碼。似乎改動(dòng)來(lái)自我繼承的那個(gè)類(lèi)。為什么基類(lèi)的改動(dòng)會(huì )破壞我的代碼? 原來(lái)是這樣…… 看看下面這個(gè)基類(lèi)(用Java寫(xiě)的,但就算你不懂Java,應該也很容易看懂):



從基類(lèi)的作者的角度來(lái)看,這個(gè)類(lèi)實(shí)現的功能完全沒(méi)有變化。而且所有自動(dòng)化測試也都通過(guò)來(lái)了。 但是基類(lèi)的作者忘記了繼承的類(lèi)。而繼承類(lèi)的作者被錯誤吵醒了。 現在A(yíng)rrayCount的addAll()調用父類(lèi)的addAll(),后者在內部調用add(),而add()被繼承類(lèi)重載了。 因此,每次繼承類(lèi)的add()被調用時(shí),count都會(huì )增加,然后在繼承類(lèi)的addAll()被調用時(shí)再次增加。 count被增加了兩次。 既然會(huì )發(fā)生這種現象,那么繼承類(lèi)的作者必須清楚基類(lèi)是怎樣實(shí)現的。而且,基類(lèi)的每個(gè)改動(dòng)必須要通知所有繼承類(lèi)的作者,因為這些改動(dòng)可能會(huì )以不可預知的方式破壞繼承類(lèi)。 唉!這個(gè)巨大的裂隙威脅到了整個(gè)繼承支柱的穩定。 脆弱的基類(lèi)的解決方法 這個(gè)問(wèn)題還得要包含和委托來(lái)解決。 使用包含和委托,可以從白盒編程轉到黑盒編程。白盒編程的意思是說(shuō),寫(xiě)繼承類(lèi)時(shí)必須要了解基類(lèi)的實(shí)現。 而黑盒編程可以完全無(wú)視基類(lèi)的實(shí)現,因為不可能通過(guò)重載函數的方式向基類(lèi)注入代碼。只需要關(guān)注接口即可。 這種趨勢太討厭了…… 繼承本應帶來(lái)zui好用的重用。 在面向對象語(yǔ)言中實(shí)現包含和委托并不容易。它們是為了繼承方便而設計的。 如果你和我一樣,你就會(huì )開(kāi)始反思這個(gè)繼承了。但更重要的是,這些問(wèn)題應當引起你對于通過(guò)層次結構進(jìn)行分類(lèi)的反思。 層次結構的問(wèn)題 每到一個(gè)新公司時(shí),我都要為在哪兒保存公司文檔(即員工手冊)而糾結。 是應該建一個(gè)Documents文件夾,然后在里面建個(gè)Company呢? 還是應該建個(gè)Company文件夾,然后在里面建個(gè)Documents呢? 兩者都可以。但哪個(gè)是正確的?哪個(gè)更好? 層次分類(lèi)的思想是因為基類(lèi)(父類(lèi))更通用,繼承類(lèi)(子類(lèi))更專(zhuān)用。沿著(zhù)繼承鏈越往下走,概念就越專(zhuān)用(見(jiàn)上面的形狀層次)。 但如果父節點(diǎn)和子節點(diǎn)能隨意交換位置,那么顯然這種模型是有問(wèn)題的。 層次結構的解決 真正的問(wèn)題出在…… 層次分類(lèi)是錯誤的。 那層次分類(lèi)應該用在哪里? 包含關(guān)系。 真實(shí)世界里有很多包含關(guān)系(或者叫做獨占關(guān)系)的層次結構。 但你找不到層次分類(lèi)。仔細想一下。面向對象范式是根據充滿(mǎn)了各種對象的真實(shí)世界建立的。但它用錯了模型——層次分類(lèi)在真實(shí)世界中沒(méi)有類(lèi)比。但真實(shí)世界里到處都是層次包含關(guān)系。層次包含關(guān)系的一個(gè)非常好的例子就是你的襪子。襪子放在裝襪子的抽屜里,然后抽屜包含在衣柜里,衣柜包含在臥室里,臥室包含在房子里,等等。 硬盤(pán)上的目錄也是層次包含關(guān)系的另一個(gè)例子——它們包含文件。 那我們該怎樣分類(lèi)呢? 仔細想一下公司文檔,就會(huì )發(fā)現其實(shí)放在哪兒都無(wú)所謂。我可以放在Documents目錄下或者放在Stuff目錄下也可以。 我選擇的分類(lèi)法是標簽。我給它加上不同的標簽。

