天天看點

艾偉:閑說繼承

繼承已經是一個古老的話題了,不過最近又在一些地方看到有人讨論它,加上自己也有一些想法,是以形成了這篇文章。

繼承好不好?

經典的OO理論說:繼承是面向對象的三大基石之一。

現代的OO理論說:組合優于繼承。

這兩種說法顯然是彼此沖突的。如果組合優于繼承的話,那麼為什麼組合沒有取代繼承成為OO的基石呢?哪一種說法更有道理?

對這個問題,簡單的說哪個比哪個更好其實是沒有多大意義的。我們應當從技術發展的曆史角度去看,這兩種說法各自是在什麼時期産生的,它們形成的背景是什麼,才能對此問題有一個更加深刻的了解。

面向對象的思想形成與上個世紀70年代,但真正在軟體開發陣營中流行開則是在80年代末和90年代初的時間。巧合的是,這一時間也正是以Windows 3.x為代表的圖形作業系統興起的時代。于是面向對象當時所面臨的主要問題就是:如何以OO的理論封裝圖形界面的開發?很多重要的早期OO思想都是在這個時期形成的,包括對于繼承的使用。

讓我們考慮一下圖形界面的特點。很容易發現:這個領域确實非常适合使用繼承,因為圖形對象天生就存在着is-a關系。比如,所有圖像對象都是Window,所有對話框都是Dialog,所有按鈕都是Button,等等。是以我們可以看到的結果就是:所有的圖形界面架構都大量使用了繼承,而且繼承的層次通常都非常深。例如,下圖是WPF中最主要的界面類——Window的繼承關系,它的繼承層次深達9層!

艾偉:閑說繼承

所有圖形架構在繼承方面幾乎無一例外。Java Swing對圖形架構由于較多使用MVC,是以繼承的深度要淺一些,但是主要的JFrame類繼承深度也達到了6層:

艾偉:閑說繼承

至此我們應該了解,為什麼早期OO理論要将繼承作為面向對象的基石了。因為當時軟體開發的領域還比較狹窄,是以很多開發者根據自己在圖形領域的開發經驗認定:繼承是OO必不可少的重要基礎,并且應當盡可能的使用。

随着曆史的發展,軟體開發逐漸進入了兩層和三層時代。程式員發現,原來在桌面應用中得心應手的繼承突然之間不那麼好用了。為什麼呢?

原因之一:兩層和三層開發的主要工作之一是對實體模組化。而現實中的實體大多數是相對獨立的,它們之間的關系更多的表現為實體之間的關聯,而不是從屬關系;

原因之二,很重要的現實問題:多層開發的主要物質基礎之一——關系資料庫,無法很自然的描述繼承關系。事實上這也是ORM出現的重要理由之一。但即使是現在最好的ORM工具,要在資料庫中描述繼承關系仍然非常複雜。這迫使程式員在相當程度上放棄了繼承;

原因之三:分層的開發方式逐漸流行開來,而繼承造成的類屬關系耦合非常不利于分層。

出于這些考慮,現代的OO理論為什麼更加推薦組合而非繼承,應該就容易了解了。

那麼現代OO理論是不是對于繼承的看法就完美了呢?我認為也不是。事實上我認為,現代OO理論存在着忽視繼承的問題,很多理論書籍隻是簡單的告訴我們優先使用組合,而根本就不告訴我們在什麼時候應當合理使用繼承,什麼時候不應當使用。這是從早期OO的過度使用繼承跳到了另一個極端,也是不可取的。

接下類我要講講對于繼承的幾個常見的錯誤觀念。

1. “組合優于繼承。”

就一般的意義上說,這個講法是沒錯的,但問題在于實在太簡略了。它并沒有告訴我們什麼情況下組合優于繼承。一個很自然的問題就是,如果組合在任何情況下都優于繼承的話,那繼承還有存在的必要嗎?

有些情況下繼承确實比組合要好。再回到圖形界面的例子,Button繼承于Window(這是早期MFC的叫法;在WinForm/WPF的分類中,Button繼承于Control,Window通常用來定義頂層視窗),這是沒有問題的,如果一定要用組合來實作Button的話,反而會導緻不必要的複雜性。之是以這種情況下繼承更好,根本原因是這裡存在着确定的is-a關系(Button is a Window)。是以我們可以得出這樣一個結論:如果語義上存在着明确的is-a關系,則考慮使用繼承;如果沒有,使用組合。

需要說明的是,這個結論其實也并不是完整的,原因我在後面還會繼續講到。

2. “繼承的目的是為了複用。”

這個說法根本是錯誤的,但就是這個錯誤說法的流行程度簡直讓人吃驚。繼承并不是為了複用,繼承的根本目的是為了對現實世界進行更好的模組化,容易複用隻是優秀模型的一個必然結果而已。我們不能倒果為因,特别是,我們不應該為了複用的目的而去繼承。

舉一個現實的例子。汽車可以複用輪子的一些特性(比如可以Run和Stop),那麼我們應當讓汽車從輪子繼承嗎?我看到真的有一些人就是這麼模組化的。但是從邏輯上想一想就知道,這是非常不合理的,汽車并不是輪子。我們建立了一個錯誤的模型,這會讓我們在以後付出代價——比如說,要讓汽車能夠換輪子怎麼辦?隻好傻眼了。

再次強調:繼承的目的不是複用,不應當為了能夠複用而使用繼承。你應當盡力去建立一個邏輯合理的模型,不應該僅僅為了友善而扭曲這個模型。

3. 隻要存在is-a關系就應當使用繼承

在第一點我說過:如果語義上存在着明确的is-a關系,則考慮使用繼承;如果沒有,使用組合。我還補充說這個結論并不完整,這裡就會說明原因。

我們還是從一個例子說起。下面是許多OO書籍都會提到的一個經典例子:

艾偉:閑說繼承

在這個模型中,Sales和Manager都是Employee,但是它們計算薪水的方法是不同的。不同的記薪方法可以通過重載getSalary()方法來實作。

這麼經典的例子有沒有問題呢?有!我們可以這樣想,“如果雇員被提升為經理,會怎麼樣?”

問題來了。在OO的世界中,對象所屬的類型是這個對象的本質屬性,任何對象在生命期間無法改變自己所屬的類别。但是現實中對象的身份很多時候是可以改變的。我們從這裡可以發現繼承的一個重大問題:一旦對象的身份發生改變,那麼繼承層次就完全崩潰了。

那麼圖形界面中為什麼可以使用繼承呢?因為圖形界面領域的對象身份是相當穩定的。Button就是Button,它不會突然變成一個頂層視窗。是以這裡使用繼承不會發生任何問題。但是對于類型可變的場合,繼承是不适合的。

從模組化的角度,我們也可以這樣了解:是Sales還是Manager,并不是一個人的本質屬性,它是可變的。一個人的本質屬性隻有他自身(姓名、性别事實上都是可變的)。我們不能夠把非本質屬性應用到繼承層次上面。

是以上面的結論應該這樣表述才算完整:如果語義上存在着明确的is-a關系,并且這種關系是穩定的、不變的,則考慮使用繼承;如果沒有is-a關系,或者這種關系是可變的,使用組合。

我們可以使用政策模式來将上面的例子重構為使用組合,如下圖所示:

艾偉:閑說繼承

從上述結論我們可以看到,繼承的使用的确是受到很多限制,在很多情況下也确實是組合優于繼承。但是不分場合、不論條件的認為組合一定比繼承好,也是過于教條主義的表現。合理的做法隻有一個:具體問題具體分析。