多重繼承不好的觀點是錯誤的 — 小評<松本行弘的程序世界>

首先得說, 一般某種語言的發明人寫的關於自己語言的東西都是非常值得閱讀的, 從別的牛人那裏你也許能學會很多奇技淫巧, 但從語言發明人那裏你能學到語言發明人本身設計的初衷, 以及設計時的一些抉擇. 這種思路是獨一無二, 絕無僅有的. 所以我在學習一個新語言時, 假如語言發明人有寫書, 一定優先閱讀.

語言發明人寫的書又分兩種, 一種是語言的教材, 這個幾乎是慣例, 因爲一個語言在初期, 沒有其他人會用的時候, 語言發明人不教會別人怎麼使用, 那可能就沒有人會用了, 而他們這本教材的好壞甚至能決定他們語言最後能有多流行. 我聽說一個有意思的言論, 說C語言之所以這樣流行, 很重要的原因就是C程序設計語言這本教材實在寫的太好了. 雖然有些誇張, 但是不無道理, 這個和現在一些開源庫對文檔要求很高的道理是一樣的, 一個開源庫的文檔寫的好不好, 對這個庫最後是不是會被很多人使用有極大的關係. 當然, 也有反例, 鬼知道Anders Hejlsberg是怎麼把一本C#的語言的教材C#程序設計語言, 寫的就像是語言的規格說明書一樣.
另外一種, 那就是除了教材外, 寫關於自己語言的設計想法的書, 這種書就更加少了, 就目前而言, 我只看到過Bjarne Stroustrup寫的C++語言的設計與演化和這本松本行弘的程序世界. 歡迎大家給我推薦其他的類似書籍. C++語言的設計與演化講述了BS在設計C++時的各種抉擇, 看完該書後, 我第一次明白C++到底是怎麼設計成那麼難用的, 當然, 同時也明白了怎麼使用C++是正確的. 後來還反覆的讀了好幾遍, 工作後, 也極力的推薦給使用C++的同事閱讀, 事實上, 現在我買的這本書就還在我原來的同事那裏, 雖然我已經離開原公司了. 如果說, 那個時候讀C++語言的設計與演化還太無知(我那時候纔剛剛參加工作), 並且C++又是我接觸編程的第一門語言, 導致碰到一些可能習以爲常的東西還如獲至珍, 所以才這麼喜歡的話, 那松本行弘的程序世界在我工作五年, 已經或多或少學習了不下於10門編程語言後, 仍然能給我很多啓發, 就更加值得向大家推薦了.

Ruby的繼承體系

多重繼承不好的觀點是錯誤的
– 松本行弘

這是我感覺是全書最精彩的部分, 本文也主要關注這一部分. 特別是對於多重繼承部分的描述, 充分體現了一個優秀的程序員, 語言設計者的功底.

講到這裏, 我想先講講我對這部分內容的認識歷史.
C++語言的設計與演化中, 我們知道, 多重繼承在加入C++之前就有爭議, 但是BS堅持自己的意見, 最終C++還是支持了多重繼承. 並且爲被很多人批評的多重繼承進行了辯護, 他的確承認了多重繼承可能帶來的混亂, 但是認爲, 在有的時候, 多重繼承的抽象的確是有意義的, 並且這種抽象在有的時候會更加優美. 本着C/C++的設計原則, “程序員總是對的”, 不應該人爲的去限制程序員, BS堅持的提供了多重繼承這一工具, 並讓程序員去決定, 什麼時候用會導致混亂, 什麼時候用可以讓設計更加優美.
在成爲標準以後, 多重繼承的爭議一直沒有消失, 各種支持多種繼承的語言甚至在怎麼解決菱形繼承上出現了不同的解決方案. 比如C++中, 菱形繼承還引入了一個更扭曲的虛繼承, Python支持多種繼承, 並且因爲所有的對象都繼承於共同的基類Object, 導致任何多重繼承其實都是菱形繼承, 當你真的開始自己用多重繼承時, 其實繼承結構已經是一張網了, 對此, Guido Van Rossum的答案是用深度優先搜索, 靠基類排列的順序來決定, 個人覺得不僅沒有解決這個問題, 反而顯的更加神奇.
而無數的JAVA用戶者則是把C++的多重繼承當作笑話和C++混亂的根源, 自豪的宣佈JAVA沒有這個問題. James Gosling給出了他自己的解決方案, 那就是沒有沒有C++自由的多重繼承, 只支持對實現的單繼承, 並引入了接口, 只允許接口的多重繼承. 並且, 在繼承接口和繼承基類形式上特別加了一些區別, 所以實際中, 很多人並不把這叫做多重繼承, 並宣稱JAVA沒有多重繼承.
有意思的是, 高老頭後來去了Google, 而我使用C++時儘量遵循的規範Google C++ Style Guide中就明確的規定了禁止C++的多種繼承(不代表這兩個事情有任何關係), 並且在Style Guide中自己給出了他們的Interface定義, 只允許使用此Interface進行類似JAVA的接口多重繼承. 這真是趣事, 在一個Style Guide中, 去強行修改一個語言特性, 也足以證明C++的多重繼承有多麼臭名昭著了.

就個人感受, 以前開發的一個iOS遊戲項目, 僅僅開發了半年, 就因爲代碼混亂不堪, bug多到無法維護, 項目一度在另外一個工作室被cancel, 然後我們中途再接手開發, 並且把多重繼承看作是混亂的根源, 後來整理代碼的很大部分工作就是用組件的方式去替代掉混亂的繼承, 並且改善後設計清晰了很多, 再後來, 我開發一個Android項目的時候, 第一次真的在項目中去使用JAVA, 深刻的體會到了JAVA對繼承的限制, 也真的去實踐了GOF在設計模式中早就提到的”組合優於繼承”這一設計原則, 也去體會了JAVA社區提倡的面向接口編程. 事實上, 我也逐漸的感覺到了, 其實, “繼承本身就是一種強耦合”, 就是一種子類對父類的依賴耦合, 在一些書籍中提到, 甚至繼承本身都是不提倡的, 也就是說, 提倡的是基於對象的設計(OB), 而不是面向對象的設計.(OO)

故事到這裏就是我的認識了, 鑑於我的認識都來自於各種上述提到的流行語言和經典書籍, 我想大部分人可能會有和我類似的觀點吧. 但是Mats不這麼看.

Mix-in

本來只是爲了跨越繼承層次來共享代碼, 現在卻需要另外生成一個獨立對象, 並且每次方法調用都要轉送給那個對象, 這實在是不太合理, 而且執行的效率也不高.

Mats直說JAVA對多重繼承的解決方式不夠方便, 也不太合理. 他提供的解決方案是, Mix-in.
Mix-in按照以下規則來限制多重繼承.
通常的繼承用單一繼承
第二個以及兩個以上的父類必須是Mix-in的抽象類

Mix-in類是具有以下特徵的抽象類.
不能單獨生成實例
不能繼承普通類

通過這種在接口和普通多重繼承之間的折衷, Mix-in提供了對實現的多重繼承, 同時對其進行了限制, 使得繼承結構不會成菱形, 而是和單一繼承一樣的樹型. 在Ruby中, Mix-in是通過模塊(module)的概念來實現的. 作爲例子, Matx用了Ruby中的Stream實現的例子.

Ruby的Stream結構

Ruby

C++的Stream結構

Cpp

從這點看, 爲什麼我說這是一種折衷方案呢, 因爲相對於只允許接口的多重繼承來說, 實現更加方便了, 但是, 對於真正的面向對象來說, 畢竟還是需要把一些實現給拆分成更細粒度的類, 才能符合Mix-in的要求. 比如看上面兩個圖就能發現, 同樣的功能, C++需要的類要比Ruby需要的少. 從這點來說, Mats批評JAVA的組合方式爲了共享代碼需要生成一個獨立對象是不方便的, 而實際上Max-in也會相對真正的多重繼承來說需要更多的獨立對象, 只是使用的方式不是組合, 而是繼承. 當然, 這種限制能帶來類似JAVA的更加良好的設計, 避免菱形繼承及類似更加複雜的繼承體系, 同時, 又比JAVA那種方式更加方便, 大家都知道, 加一個繼承只要一個單詞, 而加一個對象的組合調用, 往往需要增加N個函數接口, 以及N個調用.(雖然有種東西叫做委託) 有趣的事情是, 在編程這件事情上, 很多時候, 折衷的方法卻往往是優秀的方法…

本書不足

  1. 作者在前言裏面介紹, 是在雜誌上連載的文章的基礎上編輯修改來的, 所以有些地方其實能發現明顯的重複, 甚至不僅僅是文字內容重複, 連Enumerable的那張表都原封不動的重複了兩遍.
  2. 後面的章節趣味性有餘, 但是感覺思想性稍微有些弱. 所以我看的時候過的比較快, 不過這也見仁見智吧, 也不排除我對相關內容興趣不足的因素.

額外的聲明

我在最新的文章中把所有關於書的鏈接從豆瓣改爲亞馬遜了, 假如你點擊我的給的鏈接過去並且買了書, 亞馬遜會給我一些佣金, 當然我知道這沒有多少, 但是聊勝於無吧, 以後的文章類似, 不再聲明瞭. 我甚至都不知道這樣無害的事情爲什麼需要聲明, 但是好像在中國你需要這樣做, 因爲別人都這麼做了.

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