這文章就像我本身寫的同樣,太多觀點。java
原文見:http://mp.weixin.qq.com/s?__biz=MzA3NDM0ODQwMw==&mid=402191772&idx=1&sn=19e48d56a6fcd0616e3dc597cd52808f&scene=21#wechat_redirect面試
最近寫了些和函數式編程的文章,有讀者和我討論函數式編程和麪向對象編程的優劣。兩者都是很好的編程思想,都在着力解決代碼重用的問題,也彼此吸取對方的優勢,因此大可沒必要去分個高下。然而,編程
我面試過許多號稱精通面向對象編程(好比:Python / Ruby / C++)的工程師,隨便問幾個問題,就能夠看出這我的對面向對象的理解:數據結構
你以爲在面向對象編程中,最重要的思想是什麼?app
若是有人說起「繼承」,我會讓她寫個她在工做中使用繼承的例子。ide
若是有人說起「多態」,我會讓她解釋一下多態,並讓她寫個她在工做中使用多態的例子。函數式編程
若是有人說起「代碼重用」,我會讓她談談她對代碼重用的理解,並附上一個工做中重用的例子。函數
對第一個問題,不少人回答繼承,有些人會添上接口,多態等概念,不多人會說起代碼重用。操作系統
對第二個問題,幾乎 80% 的人都會寫出對象繼承的代碼,而這裏面有一大半的人寫出的「工做中」使用繼承的例子居然是某著名垃圾書中的經典誤人子弟的例子:Duck 繼承於 Bird,Bird 繼承於 Animal。翻譯
對第三個問題,幾乎全部人都是寫出對象繼承中的多態,而後通常的人給出的仍是那本著名垃圾書裏的著名例子:鳥能飛,也會叫,鴨子呱呱呱但不會飛。你能夠把鴨子對象賦給鳥,讓它發出呱呱呱的叫聲。
對於第四個問題,嗯,若是有人回答到第四個問題,並能信手拈來 Iterable,Comparable,這我的若是沒看『程序人生』的文章(嘿嘿),那就是基礎知識和編程思想已經八九不離十,能夠聊對象之外的東西了。
面向對象編程和函數式編程中最基本,也是最重要的要素是 代碼重用,或者更嚴格地說 代碼被重用。這裏爲何要嚴格區分代碼重用和代碼被重用呢?代碼重用根本不是個事兒,除非使用匯編語言,不然你寫十行代碼,九行都是在重用別人的代碼 —— 連往 console 輸出這樣的簡單語句,你是否是都用了語言自己的庫函數?就算語言將其納入核心語法,這個語言的實現也一定在底層調用了 libC 的某個函數,來獲取操做系統對此的支持。因此說,幹編程這一行,重用別人的代碼,咱們最在行不過;而讓本身的代碼被別人重用,咱們每每底氣不足。
寫過半年以上 Python 的,應該都知道所謂的 magic function,若是你的類定義實現了某個 magic function,那麼類就會擁有一些神奇的能力,好比:
上述的類並無繼承任何已知的類(隱式繼承 object 不算),然而它能夠很容易被別的代碼用一種公共的方式調用:
這即是代碼的被重用的能力。這種編程的方式,與其說是面向對象編程,不如說是面向接口編程。對象在這裏只是一個幌子,其存在的意義更多地是知足某種接口。在面向接口編程中,接口繼承要遠重要於類繼承。
爲何面向接口編程如此重要?由於它是一種控制反轉 —— 經過抽象出一系列接口,並在這些接口上進行操做,使得控制邏輯不依賴於具體的實現;同時,具體的實現能夠並不關心控制邏輯如何使用本身,它們會在須要的時候被調用。由此,使用對象的邏輯和對象自己充分解耦,由接口這座橋樑將兩者聯繫起來。這樣,代碼獲得了最大程度的被重用。
談到接口繼承,不得不提 Liskov substitution principle(里氏變換原則),wikipedia 這樣介紹它:
It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S.
這也是面向對象編程中常說的 Substitutability(可替換性)。這段話翻譯過來講就是,在類型系統中,若是類型 S 是類型 T 的子類型,那麼類型 T 的任意對象能夠被類型 S 的對象替換,且不影響程序的正確性。里氏變換是面向對象編程(其實適用於任何編程思想)很是重要的一個原則,也是程序得以多態的基石。若是咱們作一個系統,要注意盡一切可能知足這一原則。
什麼是多態?wikipedia 是這麼解釋:
polymorphism (from Greek πολύς, polys, "many, much" and μορφή, morphē, "form, shape") is the provision of a single interface to entities of different types. A polymorphic type is one whose operations can also be applied to values of some other type, or types.
因而可知,多態指的是咱們能夠對一個操做(接口)使用多種數據類型。多態並不僅僅是是面向對象編程的一個概念,函數式編程裏面,多態也處處可見。咱們知道在 Python 裏,一個數據結構能夠被 map 的前提是其實現了 __iter__
,而在 clojure 裏,一樣的,一個數據類型實現了 ISeq,它就可使用 map
,而在 elixir 裏,Enum.map
須要實現 Enumerable protocol。可見,多態並不是是面向對象的專利。
上文中咱們調侃的那個「鳥能飛,也會叫,鴨子呱呱呱但不會飛」的所謂面向對象的例子實質上破壞了里氏變換原則。它讓你的代碼沒法享受多態的好處。好比說,你圍繞着「鳥」這一基類打造了一個系統,系統的後期開發者用「鴨子」繼承了「鳥」,當你的代碼執行「飛」這一操做時,因爲鴨子不會飛,系統有可能會崩潰(或者拋出異常)。這時你不得不回過頭來修改圍繞基類打造的系統:當「飛」這個操做運行時,處理一切異常(有時這並不可行)。這就違反了 Open close principle,你爲了上層的一個蹩腳的實現,而不得不去修改底層的代碼。系統中相似的狀況越多,系統的 BUG 就越多。
爲了符合里氏變換,咱們須要子類嚴格繼承和實現父類的接口,這就帶來了一個問題:「類」這個概念在實際使用中,每每被當成一種分類法(taxonomy),也就是 is-a。is-a 是面向對象裏面的一個坑,由於咱們在判斷一個東西是不是 is-a 時,經常使用咱們生活中的邏輯概念去判斷,這是很是不可靠的,會出現不少蹩腳設計 —— 好比做爲父類型的「鳥」會飛,而子類型的「鴨子」不會飛這樣不符合里氏變換但符合生活邏輯的設計。使用「鳥」和「鴨子」來說述面向對象的繼承關係,雖然很直觀,很形象,卻讓初學者陷入一個泥潭而不能自拔。
Scott Wlaschin 在他那著名的 Funtional programming patterns 中提到,types are not classes。把「類」(class)等同於「類型」(type)也是初學者經常碰上的坑。在函數式編程裏面,類型其實是一種接口,它是數據和數據能夠產生的行爲間的一座橋樑:
而「類」是「類型」的一種實現方式。從這個意義上講,「會飛」(flyable
) 是一個類型,「鳥」實現了 flyable
,而「鴨子」沒法實現 flyable
,因此「鴨子」並非「鳥」的子類型。弄明白了這一點,咱們就不會傻乎乎地去根據生活經驗,把「鴨子」繼承在「鳥」的名下。
而弄明白了這一點,咱們也就能夠參悟出 java 的設計者爲什麼煞費苦心地爲 class 的類繼承使用 extends,而接口繼承使用 implements,同時如若 override 方法,返回類型必須是 covarient 了。