閑說繼承
繼承已經是一個古老的話題了,不過最近又在一些地方看到有人討論它,加上自己也有一些想法,因此形成了這篇文章。
繼承好不好?
經典的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關系,或者這種關系是可變的,使用組合。
我們可以使用策略模式來將上面的例子重構為使用組合,如下圖所示:
從上述結論我們可以看到,繼承的使用的確是受到很多限制,在很多情況下也確實是組合優于繼承。但是不分場合、不論條件的認為組合一定比繼承好,也是過于教條主義的表現。合理的做法只有一個:具體問題具體分析。