值得懷念的世界,卻不值得回去!
——莊表偉
面向過程的世界是完整的,統一的,也是容易理解的——對于程序員來說——或者說他只需要一種理解能力。這個世界雖然值得懷念,卻不值得再回去。因為,我們不再像當年的程序員那樣,只開發那些簡單的軟件了。很多人崇拜那些早起的“大牛”,其實平心而論,我們現在面對的問題的復雜程度,在他們當年可以說幾乎無法解決。需求的復雜程度也不是他們當年能夠設想到的。
這是在秘魯發現的神秘的納斯卡巨畫,這樣巨大的地面藝術,可以給我們對于面向過程的編程的結論一個可視化的比喻。面向過程的編程,只有一個統一的世界,他對于軟件的理解,始終不會離開計算機的操作與運算本質,這就像在平地上作畫那樣,我們需要的一根長1米的直線,非常容易,兩點一線,一拉就出來了。但是當我們需要在地面上畫一根5000米甚至更長的直線時,如何保證畫出一條直線,就成為一個巨大的挑戰。當視角無法升到足夠的高度時,如此復雜的圖案幾乎是無法把握的。僅僅依靠結構化的劃分,并不能完全的隔離復雜度的交互影響。單步跟蹤一個1000行代碼的程序并不困難,但是如果是100萬行代碼,甚至更多呢?
再看一張照片:

這是世界上最大的“埃及胡夫金字塔”。我們假設,如果當年法老在工程進行到80%的時候,提出需求變更,希望金字塔尖能夠向右移動10米。情況會如何?——會死好多勞動人民的!如果希望向右移動100米呢?如果希望有四個塔尖各在一個方向呢?如果。。。還好這一切都沒有發生,否則我們就不可能看到一個真正完工的金字塔。然而在軟件開發領域,當“結構化編程”面對“移動金字塔”的需求變更時,它只能破產!
可以得出一個比較關鍵性的結論是:
僅僅從計算機的角度出發,對于更為復雜的需求,描述力不足。對于巨大的需求變更,應變力不足。而這正是對于的軟件需求的必然發展趨勢。
所以,那個世界不值得回去,但是,OO真的幫到我們了嗎?
多年以后的今天,我們依然在思考這樣一個問題:“OO怎么就流行起來了呢?”學術一點分析,這個問題可以分為以下幾個部分:
1、OO之前的軟件開發困境何在?
2、當時的開發人員如何解決那些困境?
3、那些解決困境的努力,為何會匯入OO的名下?
4、OO這個概念,從何而來?
5、OO的核心內容是什么?
6、OO的實際目的是什么?
7、OO的理想目標是什么?
困境
“需要一個超越于機器執行層面的的認識。”或者說,不能僅僅以“解空間”的語言描述解決方案,最好能夠以“問題空間”的語言描述解決方案。這是OO得以流行的真正動力,因為OO宣稱自己能夠更好的描述“真實世界”。
注意我要區分的幾個概念:“解決困境的努力”、“困境的根本原因”、“OO所宣稱的目標”、“OO實際達到的效果”。因為在以往的OO的宣傳中,這些概念是一個有機的整體,而卻認為,其中有諸多“斷裂破碎”之處。
面向過程的編程,面對的困境其實相當多,最根本的原因前面也已經指出了。但是在當時,在具體的項目中,在特定的人看來,他們碰的,是各自不同的問題。在人工智能領域,在圖形化界面領域,面對的是模擬的問題。在企業應用領域,面對的是數據訪問與保護的問題。從共同的困境來看,適應變更,方便重用,系統健壯之類的要求,也是需要考慮的。
概念的發展歷程
首先聲明,這是一個假想的歷程,并非真實的歷史。真實的歷史,可以參考以下URL中的介紹:
http://heim.ifi.uio.no/~kristen/FORSKNINGSDOK_MAPPE/F_OO_start.html
模擬:模擬的概念由來已久,但是如何模擬卻是一個大問題。
抽象數據類型(ADT):在對結構(structure)進行簡單的擴展之后,ADT就順理成章的出現了。
封裝:對于ADT的理論總結可以表述為“封裝帶來了數據的安全性”。
繼承:一堆不能重用的代碼是“罪惡”的。繼承首先出來試圖解決這個問題。
多態:是一個意外的收獲,如果各個對象都提供同一個名字的方法,似乎就可以不去管他們的區別。
在這些努力與嘗試之后,面向對象橫空出世,從哲學的高度總結了這些努力:“你們都是正確努力的一部分,你們都在試圖更好的描述真實世界,真實的世界里一切都是對象,所有的對象在一個分類系統里各安其位,對象之間通過消息相互‘招呼’。應用OO思想,描述真實世界的運行,就是編程的主要工作。”
但是事實上呢?編程并不是描述真實世界!而是描述需求世界。不同的需求世界,需要不同的“世界觀”。這一點,面向對象并沒有考慮到。當時流行的思想是通用編程語言,使用一種語言解決世界上所有的開發難題。而要整體解決各不相同的開發難題,只能將目光投向“真實世界”,那是各個差異巨大的“問題空間”的唯一一致的背景。
面向對象的哲學破綻
在此特別感謝徐昊,這一部分該如何寫,我始終沒有想好,在與他討論之后,我基本理出了一個思路。
面向對象有兩個哲學基礎,原子論與形而上學。這兩大基礎,在哲學的發展歷程中,曾經如日中天,無可置疑(在古希臘那時候),如果說這樣的哲學不偉大,那就是我太狂妄了,但是如果有人說:“西方哲學在之后的幾千年里沒有進步,古希臘哲學就是西方哲學的頂點,因此面向對象理所當然的應該建立于這兩大哲學之上!”你會相信嗎?
1、原子論
西方哲學的發展,經歷了兩次變革,一次是認識論轉向;一次是語言轉向;第一次轉向使哲學的基礎從本體論和形而上學變為認識論,從研究超驗的存在物轉到研究認識的主體和主客體關系;第二次轉向把對主客體的關系的研究變成了對主體間的交流和傳達問題的研究。把對主體的研究從觀念和思想的領域轉到了語言的領域(語句及其意義);這兩次轉向的代表人物分別是笛卡爾和維特跟斯坦。
————《OO, OO以后, 及其極限》waterbird
看來我可能是比較淺陋了,在我看了waterbird的《OO, OO以后, 及其極限》之后,曾經深深的自責過。看來OO沒有我想的那么土,不是直接來自古希臘哲學,而是從維特根斯坦繼承而來的。waterbird的引用而后總結的一段維特根斯坦的話,使我對維特根斯坦大為佩服。
小結2: 2 主要說明 --- 事實(facts)由原子事實(atomic facts)所組成;原子事實(atomic
facts)由更基本的對象(objects)所組成;我們的關于外部世界的主觀描述圖畫,與它所描述的外部世界具有相同的邏輯結構;注:
(這即是相當于軟件開發中的"建模")
還好,在我昨天列出了閱讀書目之后,gigix提醒我看了另外一篇文章:《維特根斯坦早期思想及其轉變》,這是一個正兒八經的哲學家的文章,總算讓我見識到了軟件開發這個行當里,頗有些不懂哲學的家伙,拿著哲學來唬人的。
原子事實是最簡單的事實,無法再從中分析出其他事實,分析的結果只能是對象。因此,原子事實是對象的結合或配置。“對象是簡單的”〔2.02〕,不可再加以分析,所以,對象就是簡單對象,不過,為清楚起見,維特根斯坦還是經常采用“簡單對象”這個說法。簡單對象這個概念引起很多困惑和爭論,其實維特根斯坦自己也很猶豫,他在筆記中寫道:“我們的困難是,我們總說到簡單對象,卻舉不出一個實例來。”
他曾考慮過關系、性質、視域上的小片、物理學里的物質點。他還說個體如蘇格拉底、這本書等,“恰恰起著簡單對象的作用”。一條可能的思路是把簡單對象理解為一種邏輯要求,一個邏輯終點:“簡單對象的存在是一種先天的邏輯的必然性。”
在《邏輯哲學論》中,維特根斯坦大致采用了這條路線,這本書里從未舉例說明什么是簡單對象。
維特根斯坦說的對象,是OO中的對象嗎?一個正兒八經的現代哲學家的困惑,OO大師們考慮到了嗎?只有樸素、甚至可以說是幼稚的原子論觀點,才會輕松的混淆:事實、原子事實、對象和具體的物質,物體。對于OO來說,對象非常容易被發現,幾乎隨手一指,都能點到一個對象。
從語言哲學來說,最為困難的是:“有沒有一種語言,可以清晰地、完整地描述這個世界?”邏輯原子論原本認為有可能。但是,維特根斯坦的后期哲學轉向,恰恰指出了一個困境,而這個困境即時是人類歷史上最為天才的頭腦,也無法“走出”的。德國人最具有理性的思維能力,而其中最為天才的頭腦卻碰上了理性思維的天花板。維特根斯坦很難理解,越是德國天才,他的語言越是晦澀。倒是從中國哲學的角度,往往能夠看透其中的掙扎。老子在幾千年前就說:“道可道,非常道,名可名,非常名”。因為試圖準確、無誤、無失漏的命名、描述世界的努力,是不可能成功的。
因此,我現在可以斷言,面向對象背后的原子論,不過是直接師承古希臘哲學的簡單、樸素、幼稚的原子論,這樣的原子論哲學,早已破產。作為哲學史的研究對象,自有其價值,而作為指導軟件開發那么現代活動的哲學理論,實在是太不適用了。
2、形而上學
當我寫下這個標題的時候,內心無比惶恐。這么大個題目,是我這個半路出家,Google成才的家伙能夠談論的嗎?多少哲學家一輩子“皓首窮經”,也不過就是研究個形而上學啊。
當初,維特根斯坦去找羅素,問到:“你看我是不是一個十足的白癡?”羅素不知他為什么這樣問,維特根斯坦說:“如果我是,我就去當一個飛艇駕駛員,但如果我不是,我將成為一個哲學家”。可見哲學這東西,只有真正的天才才有能力去研究它。
還好,我并不是要研究形而上學,我只是要研究面向對象背后的形而上學哲學基礎。
我也不是要證實這個哲學基礎的正確性與適用性。我只需要證明“面向對象背后的那個形而上學基礎是不正確的、是不適用于軟件開發的。”
面向對象的兩大核心概念是:“對象”與“類”。“一切皆是對象”是由樸素原子論而來的。“萬物皆有類屬”就是由亞里斯多德的形而上學來的。
對于亞里斯多德的形而上學理論不熟悉的朋友,可以即時補課,中國人民大學哲學系的《西方哲學史》有好幾節專門講這個方面:《亞里斯多德的實體論I》、《亞里斯多德的實體論III》。還有就是到Google上去專門搜一下亞里斯多德的邏輯學說,看完以后,咱們回來接著說。
咱們用自己的話說一下:“種”、“屬”、“屬差”以及“定義”這幾個概念。
種:是一個大的概念,假設已經預先定義好了的。
屬:所有屬于某一種的概念,都是那一種下面的屬。
屬差:同屬一種的、同一級別的屬之間的差別,或者說個性。
定義:通過種加屬差,可以定義一個屬的概念。
舉例說明:人是二足直立動物。人是一個需要被定義的屬,動物是人之所屬的種,二足直立是人作為動物的共性之外,擁有的個性,也就是屬差。
懂得初步的面向對象編程的同志們,你們都看出來了吧,大多數OO語言也是這么定義類的。你定義一個Animal,再用Person去繼承Animal。在Animal里有一些屬性與方法,在Person里再加一些人所特有的。很多很多的面向對象的教科書里,甚至就是直接用這個定義來舉的例子。
問題出在哪里?或者有人會問:“這樣有什么不對嗎?”
我們可以通過“種+屬差”來定義一個新的屬嗎?定義成立的前提是什么?先要有種的定義。然后才可能有屬的定義。種的定義又是哪里來的呢?在一個種的概念之上,必然存在一個更普遍的種,一個更大的范疇。在亞里斯多德來說,在所有的種之上的種是“存在”,而存在是無法被定義的。而在面向對象的哲學里,即使是這一個最基本的哲學困境也被忽略了,無法被定義的概念,被代換為無需由程序員定義的概念(Object)。屬差的區別在哲學家看來,是本質的,是基于深刻認識才能提出的。而在面向對象的哲學里,種的共性就是基類所定義的“屬性與方法”,而屬的個性,就是對于基類的擴展。“種+屬差”變成了“公用代碼+擴展代碼”。
當概念定義這樣一個“問題域的描述手段”,演變成“減少重復代碼原則”之后。Class繼承的概念就越發的模糊不清了。我們來總結一下:
1、面向對象原本聲稱的描述真實世界的目標,采用的工具卻是樸素的“種加屬差”的方式。
2、面向對象分析中,發現具體的對象還算是容易的,發現“種”的概念卻是困難的。
3、在實際應用中,種概念的發現與定義,被偷換為公共代碼的抽取。
4、由于基類的定義的隨意性,導致子類不但可以擴展基類的行為與特性,還可以覆蓋(改變)基類的行為與特性。
5、由于哲學概念的與開發概念的混淆,使得在OO領域IS-A、Has-A、Like-A成為最為繞人的概念。
在寫完了哲學分析部分之后,我總算是喘了一口氣,仿佛穿越了最幽暗的深谷,終于走出了自己最不擅長的領域了。
后來在MSN上和曹曉鋼聊了挺長時間,對于OO的批判,他認為有點過頭了。經過我的解釋,他提出了一個更好的建議,清楚的說明自己批判的OO,究竟是哪一個階段的OO,然后才不至于誤傷到已經改善過后的OO。所以我打算整理一下對于OO發展階段的看法,寫在下面:
1、面向對象的語言:先有語言
2、面向對象的分析與設計理論:再有理論
3、面向對象的設計原則的全面總結:再有原則
4、設計模式的初步提出:然后才有了真實的經驗總結
5、重構方法的提出:然后才考慮到代碼設計的細節上的改善
6、AOP概念的提出:打破OO封裝的“封印”
7、新語言的出現:Python、Ruby之類面向對象的動態語言:更加方便的語言?
8、ASM、CGLIB、Mixin之類技術的出現:OO喪鐘的先聲
具體的對于各個階段的分析,將在隨后展開,目前對于OO的哲學分析,基本上是針對原始的OO概念的。隨后的OO技術的發展,也在試圖解決由于OO的哲學基礎假設帶來的問題,當然,越是解決問題,也就離OO的本意越遠,現在有人還以為OO在不斷發展,而事實上,OO早就盛極而衰,目前已經處在破產的前夜了,我的這篇文章,就是打算使這一天,早日到來!
類型系統
“事實上,我們猜想是,如果沒有知識表示和自動推力工作的幫助,這些問題(指類,繼承)是無法僅僅通過計算機語言設計的方式來處理的。”——SICP
2.5,中文版136頁,角注118。
Elminster那篇論述,正好和我的文章形成一個互補關系,他以極為清晰的表達語言,說明了OO打算以類型化方式描述真實世界,所面臨的難題。這也使我不必再次動腦子思考如何回答JavaCup的哲學方面的疑問了。而下面這一段話我想特別再次引用一下:
就我個人來說,比較傾向于認為這條最終是走不通的死路,人是從事物的特征與屬性歸納出它的“類型”,而不是相反。某種意義上說,“類型”是為了節省描述時間而產生的 ……
唔,這個太遠了,所以就此打住。
大家記住這段話中的,特征、屬性、類型這幾個關鍵字。我先繞個小彎再回到這個話題上來。
我之前分析的面向對象的哲學漏洞時,也有不少朋友認為,說:“面向對象不能很好的描述真實世界,并非一個有意義的指控。OOA、OOD本來就是用來對需求建模的。也就是打算描述需求世界。”
其實我的指控分為兩個階段,一方面,OO所依據的哲學導致了軟件開發的苦難,而且至今余毒未清。另一方面,即使是指打算對需求建模,OO的技術手段也是有缺陷的。
就這么說吧:OO的類型系統,原本是從ADT來的。一個抽象數據類型,將數據與操作封裝在一起,是出于對于數據被“莫名其妙的修改”的擔心。但是,結果呢,一個ADT如果不支持繼承,那么它的代碼就無法被重用。所以OO就在ADT的基礎上增加的一個繼承,通過繼承的方式來復用以有的代碼。這樣的思路原本沒有太大的問題,如果它僅僅只想表達各種自定義數據類型的話。
但是在OO的哲學提出之后,一切皆是對象,所以一切出于類型體系之內,對于數據類型的定義,擴展到了對于現實世界中各種實體的類型定義,整個一個類型系統,其內在的語義大大擴展復雜化了。更糟糕的是——引用Elminster的話是從事物的特征與屬性歸納出它的“類型”——而因為OO封裝也就是隱藏了內部數據,事物的特征與屬性,從其本質屬性,被轉義為對外的提供的操作接口。但是,要分析一個實體的本質,而不是實體的外部表現,更不僅僅是“我能對他做什么”。這才是實體分析有可能成功的關鍵,而在OO的語言設定中,這卻是難以做到的。
我們來看兩張圖片:

這是在SICP里討論類型系統的第一張圖片,我稱之為“OO成功案例”。

這是在SICP里討論類型系統的第二張圖片,我稱之為“OO失敗案例”。
為什么一個能夠成功,而另一個卻會失敗?以往的解釋其實比較“直覺”。看著這個圖,就想當然的以為:“這是因為多重繼承導致的!”事實上呢?
第一張圖中所顯示的成功,很多人會認為這是由于這一個對象塔中的每一個對象都能夠支持加減乘除運算。而在幾何圖形中,這樣一致的操作接口不存在了。而事實上,正是因為復數、實數、有理數、整數,在本質屬性上有一致之處,他們才能表現出一致的“可加減乘除性”。而不是相反。當我們畫出第二張對象關系圖的時候,也不是根據幾何圖形可以接受的操作類型來進行分類與顯示繼承關系的,而是根據不同的幾何圖形的本質屬性的相近程度來劃分類型體系的。多邊形的本質是:
“多條有限長直線組成了一個封閉圖形”,而三角形與四邊形的本質則是,邊的數量分別為三和四。等腰三角形的本質是,不但邊的數量為三,而且其中有兩條邊的長度相等,直角三角形的本質是不但邊的數量為三,而且其中有一個直角。如此等等......
各位,請再次思考這樣的分類體系的內涵。
我的結論是:“一個類型,是由其本質決定了所能表現出的可操作性,而不是有其所能接受的操作決定了其本質。然而,OO正好把這個問題搞反了!”
繼承、重用、多態
OO的核心思想就是繼承,那么,OO為什么要繼承呢?對于這個問題,OO的理論大師們有好多個版本的解釋:
1、“這是OO的類型系統自然的要求。設想一下生物學的分類系統:動物——>哺乳動物——>靈長類動物——>人類。或者設想一下我們的概念系統:機器——
>交通工具——>汽車——>小轎車。這樣的現象在你的生活中難道不是隨處可見嗎?”
2、“如果你有一個類,叫做車輛,這個車輛類能夠移動,現在你要建立一個子類,叫做家庭型轎車,你就可以直接繼承車輛這個類,而不需從頭再寫移動部分的代碼了呀!”
3、“如果你有三個類,三角形、四邊形、正方形,他們都需要顯示在屏幕上,那么你就可以建立一個基類叫多邊形,提供一個draw()方法,然后讓那個三個類都繼承這個多邊形類并且覆蓋那個draw()方法,這樣,你就可以在繪圖的時候,統一對一種多邊形類進行操作,不用去管那個對象究竟是哪一種多邊形。”
這三種解釋,是最為典型的OO繼承的好處的解釋了。但是你如果仔細的看,就能發現這三種好處,分別描述的是:“概念的特化”、“代碼的重用”以及“接口的重用”。或者可以分別命名為:“繼承”、“重用”、“多態”。
“這樣有什么問題嗎?”,也許有人會問。問題就出在這三個好處是用一種方法提供,而這三個好處相互之間有時是相通的,有時又是矛盾的!當我們運用OO語言,來寫這樣的繼承的語句時,一切都是“攪和在一起的”!
假設Class A有兩個屬性和兩個方法:String a1;int i;void f1();void f2();當我們另外寫一個Class
B去繼承Class
A的時候,我們可以繼續使用某些屬性,而覆蓋另一些屬性,還可以繼續使用某些方法,而重寫另一些方法。還可以添加一些新的屬性,還可以添加一些新的方法。如果在加上各種訪問控制符的限定與修正。誰能夠告訴我:“這個Class
B究竟想干什么?!”
也許有人會繼續為這樣的現象辯解:“這是對于繼承的誤用,正確的OO程序不會這樣的!”
但是,這個辯解并不成立,幾乎所有的OO的編程語言,都沒有在繼承問題上做出太多“非此即彼”的限制,原因只有一個,那就是,在某些特定的場合,這樣的“拼盤”是相對最合理的編碼方式。
我們前面還沒有提到多重繼承,一個允許多重繼承的語言,會讓這個問題更為復雜,也可以說會使得場面越發的混亂。讓我們舉一個例子,這是Eiffel語言的繼承語法,讓我們看一看面對繼承這樣一件事情,一個程序員,究竟需要考慮多少問題。來源是《對象揭密》,我就一邊抄,一邊直接翻成中文了。
繼承 :inherit 父類列表
父類列表 :{父類 ";" ... }
父類 :類名[特性適配說明]
特性適配說明:[Rename] :重命名以消除名字沖突
[New_exports] :重新設定特性導出的分組
[Undefine] :撤銷定義
[Redefine] :重定義以取代虛函數
[Select] :更加高級的功能
end
最值得看的就是這個特性適配說明,更加深入的說明還是各位自己去找書看吧。這就是號稱優雅的解決了OO繼承問題的Eiffel語言。他所謂的優雅,可以不客氣的說,就是把所有的麻煩都明確的告訴你,而不是像C++和Java那樣,假裝什么事情都沒有發生過。但是,麻煩依然在那里,并沒有減少,根本的解決方法,是應該不讓這樣的麻煩出現呀!可是OO確實無法做到這一點。
因為他把數據和操作封裝在了一起,然后又偷換了實體本質的概念,在這樣的情況下的OO,他的繼承是肯定搞不好的!
接口、泛型與重用
先說點提外話,我從小學開始學習BASIC和LOGO,到后來學習了FoxBase、FoxPro、C/C++、Visual
Basic、VBScript、JavaScript、PHP,之后才開Java編程,之后也沒有再換過語言。誠實的說,只有Java語言,是我認認真真的學習和研究的。對于面向對象的理解,也是在學習和使用Java之后,才真的開始深入思考這方面的問題,在此之前,我甚至認為所有的語言都沒有什么本質的差別,面向某某和面向某某之間也沒有什么大不了的差別。
所以當我想要寫這篇文章的時候,其實內心是相當惶恐的,我對于面向對象的了解,其實只來自于一種語言,那就是Java,而Java是不是就等于是面向對象呢,只怕是不能這么說的吧。
JavaEye有人留言:“不要到時候說不圓影響了一世英名。”;“討論這個問題,我還是建議去看 SICP,你會發現所有OO具有的思想SICP都講到了”;“實際上我很懷疑莊某最后還是跑不出SICP的框架,如果是這樣,那么其理論的價值就要打折扣了。”我那個慌啊,趕緊到書店去買了SICP回來仔仔細細的啃,然后再在MSN上向T1好好的請教過幾次,最后總算放心了,我的思路,不在SICP的框架內,或者說,不在SICP作者的思考局限之內。
還有人留言,提到了C++:“OO門檻較高是不爭的事實。的確很多人并沒有進入。有句話可以套
用,沒有三年的功底,最好不要說懂C++。幸運的是這門東西的回報,會告訴你所付出的是完全值得的。”我又慌了,C++背后的面向對象,何等高深,我卻從來沒有用C++做過哪怕1000行以上的程序,這等門檻都沒有入的人,有資格評價面向對象那么大的事情,趕緊的,我又到書店去買了一本《對象揭秘》,我對于當年gigix的一篇介紹《編程語言的宗教狂熱和十字軍東征》始終記憶猶新,里面提到了面向對象,提到了C++的無數缺點,還提到了Eiffel,一個據說是比C++要好無數倍的面向對象的語言。如果我要想加快速度,又想保證質量的話,從《對象揭秘》里面應該可以找出很多現成的彈藥吧。
抱著急于求成的功利主義目的,我開始仔細看這本《對象揭秘》,一看之下,真是大有收獲:
*C++果然毛病多多,而且作為第一個大面積流行的OO語言,OO的實際含義更多的來自于C++。
*Java的毛病少了很多,因為它引入的一些概念,不再使用的一些概念,大大的減少了C++式OO編程的陷阱,只是這樣一來,在復雜問題上使用Java語言,往往會寫出很丑陋的程序。
*Eiffel同樣也是反思C++缺點的語言,但是它的改進基本上是表面的,Java是使問題簡化,哪怕犧牲語言的表達能力,而Eiffel是使問題表面化、集中化,陷阱雖然沒有了,但是問題一個都沒有減少,反而因為“讓人眼花繚亂的復雜語法”,讓人望而卻步。
*《對象揭秘》是一本很一般的書,作者花了十多年的時間攢出一本書來,實質上還是BBS里一段一段討論的水平。
————————————————————————————————————————
好了,題外話結束,接下來討論正題,今天主要研究OO的概念中兩個較為邊緣的概念“接口”與“泛型”,以及探討一個實際上最為重要的誤用——“重用”。
1、關于“接口”
接口是什么東西?接口是一個很奇怪的東西!接口之所以奇怪,因為他的來龍去脈實在是讓人看不懂。基礎的OO概念中,并沒有接口的位置,因為按照“經典的”面向對象思維,一個沒有代碼、沒有內部數據,只有方法說明的東西,是無法理解的。
追根溯源的說,首先是在C++的面向對象實踐中,發現了對于“抽象類”的需要,為什么需要抽象類呢?因為代碼重用的需要,比如一個基類,其實就是一堆公用代碼,有一個名字把它框起來叫一個類,但是完全沒有道理把它實例化。像這種“發育不完全的類”,該拿它怎么辦呢?OK,就讓它不能“出生到這人世間來”。抽象類的本質就是這個樣子的。
到了Java,因為對于多繼承的恐懼,Java完全擯棄了多重繼承,這是Java被攻擊得最多的地方,因為這樣的單根繼承,實在是因噎廢食——因“怕被繼承體系搞亂”廢“更加方便道代碼重用”。于是Java就說了:我們有一個“安全的”多重繼承,叫做“接口”,這個接口,完全沒有代碼,只有說明,所以絕對安全,但是由能夠實現多重繼承的好處云云。
而事實上呢?多重繼承的根本目的,并不是像Java所宣稱的那樣為了“同時繼承多種類型”,而是為了“同時重用多組代碼”。接口這一發明,完全不能達到多重繼承的代碼重用效果,卻被宣稱為多重繼承的替代品。其實質是:“從一個發育不完全的實體,變成了一張徹底沒有發育的皮”。
最為令人感到奇怪的,還不是“接口的出現”,而是“面向接口編程”的出現,Java被冠以“面向接口的語言”的美名,“面向接口設計”成了OO的設計原則之一,“針對抽象,不要針對具體”,成了OO名言之一。實在是......
關于OO的設計原則,我下面還會專門討論,這里先指出一個大漏洞:“抽象類的那個抽象,和與具體相對的那個抽象,根本就不是一回事!”
繼承、多態與泛型沖突的一個例子
寫技術文章,例子其實很難舉,特別是找到有殺傷力的,決定性的例子,相當困難。昨天我接著看《對象揭密》,總算被我找到一個,當然,它那上面的解說,實在是比較模糊,因此我決定用自己的話重新敘述一遍,代碼示例用Java的泛型語法,但是要表達的意思,確實所有的具有泛型的OO語言都需要面對的。
java代碼:
public class X {
protected int i;
public void add(int a){
i=i+a;
}
}
public class Y1 extends X {
public void add(int a){
i=i+a*2;
}
}
public class Y2 extends X {
public void add(int a){
i=i+a*3;
}
}
這是三個最簡單的類,Y1和Y2都繼承了X,并且重寫了add函數。當然,這只是舉例,實際上這三個add中,有兩個是不合理的。
java代碼:
ArrayList listx=new ArrayList();
ArrayList listy1=new ArrayList();
ArrayList listy2=new ArrayList();
listx.add(new X());
listx.add(new Y1());
listx.add(new Y2());
listy1.add(new Y1());
listy2.add(new Y2());
這幾行代碼都非常簡單,只是為了說明一個道理,在ArrayList和ArrayList中,能夠放的就只有Y1和Y2了,而在以X為泛型的ArrayList中,就可以放X、Y1、Y2了。而當然了,這樣的用法,只怕是不合泛型的目標的,本來就是希望能有一個類型的自動檢查與轉換,都放在ArrayList中,幾乎就等于都放在ArrayList中了。
現在我們有這樣一個需求,對于得到的ArryaList,能夠一一調用里面的對象的add(int a)方法,當然了,只要這個ArrayList里的對象都是X或者X的子類就行了。我們可以寫出這樣的代碼:
java代碼:
public void addListX(ArrayList listx){
for(int i=0;isize();i++){
X x=listx.get(i);
x.add(1);
}
}
是不是很簡單?且慢,這個addListX函數,我們能夠把listx傳遞給它,但是能不能把listy1和listy2
也傳遞給它呢?如果我們能夠把listy1和listy2傳遞給它,就相當于執行了如下的類型轉換代碼:
java代碼:
ArrayList listy1=new ArrayList();
ArrayList listx=listy1;
這樣做行不行呢?在Java和C++中,是不行的。也就是說,如果我們要想只寫一遍addListX這樣的函數,而不用再多寫兩遍addListY1();addListY2();這樣的函數,就需要把所有的X,Y1,Y2這樣的類型都放到ArrayList這樣的容器里,否則,addListX函數,是不接受ArrayList和ArrayList類型的。即使Y1和Y2是X的子類型,ArrayList與ArrayList也毫不相干。不能相互轉換。
有人也許會說,為什么這么限制嚴格呢?Eiffel就沒有這么這么嚴格的限制,他允許ArrayList自動轉型為ArrayList,這樣是好事情嗎?如果listy能夠被轉型為ArrayList,那么就可以往里面添加Y2類型的對象了,這又是原來的泛型ArrayList不允許的。也就是說:除非addListX能夠保證只對listy1做只讀操作,否則,類型安全性這個泛型原本要追求的目標就不能實現了。而如果要追求絕對的類型安全性,像C++和Java那樣,那么代碼要么就得寫三遍,要么X、Y1、Y2類型的對象就得都放到ArrayList這樣的泛型容器里去。
注意看這其中的左右為難的狀況,繼承、多態、泛型,并不是真正正交的、互不干擾的,而是在一個相當普通的目標面前,他們就起了沖突了。