!——懷念失落的世界(2)
值得懷念的世界,卻不值得回去!
——莊表偉
接著昨天的話題說下去,那個面向過程的世界是完整的,統(tǒng)一的,也是容易理解的——對于程序員來說——或者說他只需要一種理解能力。這個世界雖然值得懷念,卻不值得再回去。因為,我們不再像當年的程序員那樣,只開發(fā)那些簡單的軟件了。很多人崇拜那些早起的“大牛”,其實平心而論,我們現(xiàn)在面對的問題的復雜程度,在他們當年可以說幾乎無法解決。需求的復雜程度也不是他們當年能夠設想到的。
我們來看幾張照片:
http://forum.cul.sina.com.cn/cgi-bin/view.cgi?gid=43&fid=1352&thread=1206&date=20040622&page=1
這是在秘魯發(fā)現(xiàn)的神秘的納斯卡巨畫,這樣巨大的地面藝術(shù),可以給我們對于面向過程的編程的結(jié)論一個可視化的比喻。面向過程的編程,只有一個統(tǒng)一的世界,他對于軟件的理解,始終不會離開計算機的操作與運算本質(zhì),這就像在平地上作畫那樣,我們需要的一根長1米的直線,非常容易,兩點一線,一拉就出來了。但是當我們需要在地面上畫一根5000米甚至更長的直線時,如何保證畫出一條直線,就成為一個巨大的挑戰(zhàn)。當視角無法升到足夠的高度時,如此復雜的圖案幾乎是無法把握的。僅僅依靠結(jié)構(gòu)化的劃分,并不能完全的隔離復雜度的交互影響。單步跟蹤一個1000行代碼的程序并不困難,但是如果是100萬行代碼,甚至更多呢?
再看一張照片:
這是世界上最大的“埃及胡夫金字塔”。我們假設,如果當年法老在工程進行到80%的時候,提出需求變更,希望金字塔尖能夠向右移動10米。情況會如何?——會死好多勞動人民的!如果希望向右移動100米呢?如果希望有四個塔尖各在一個方向呢?如果。。。還好這一切都沒有發(fā)生,否則我們就不可能看到一個真正完工的金字塔。然而在軟件開發(fā)領(lǐng)域,當“結(jié)構(gòu)化編程”面對“移動金字塔”的需求變更時,它只能破產(chǎn)!
可以得出一個比較關(guān)鍵性的結(jié)論是:
僅僅從計算機的角度出發(fā),對于更為復雜的需求,描述力不足。對于巨大的需求變更,應變力不足。而這正是對于的軟件需求的必然發(fā)展趨勢。
所以,那個世界不值得回去,但是,OO真的幫到我們了嗎?
敲響OO時代的喪鐘!——OO時代的來臨(1)
三、OO時代的來臨
多年以后的今天,我們依然在思考這樣一個問題:“OO怎么就流行起來了呢?”學術(shù)一點分析,這個問題可以分為以下幾個部分:
1、OO之前的軟件開發(fā)困境何在?
2、當時的開發(fā)人員如何解決那些困境?
3、那些解決困境的努力,為何會匯入OO的名下?
4、OO這個概念,從何而來?
5、OO的核心內(nèi)容是什么?
6、OO的實際目的是什么?
7、OO的理想目標是什么?
......
在今后的幾天里,我將詳細分析這些問題,得出的結(jié)論,說不定會出乎你的意料之外
!——OO時代的來臨(2)
1、困境
上一章在分析的關(guān)于面向過程編程的思維所面臨的困境,還有一篇文章可以給大家參考。在去年三月份的時候,我就打算寫一些對于OO的反思,但是覺得自己思考尚且不夠成熟,因此沒有勉強,當時的文章現(xiàn)在可以在Javaeye看到。思考軟件開發(fā)(1)——面向?qū)ο蟮那扒昂蠛?/span>
這篇文章其實只分析到了面向過程的困境,可以和前面我寫的那兩個比喻相互參照。而兩篇文字要表達的含義是一致的:“需要一個超越于機器執(zhí)行層面的的認識。”或者說,不能僅僅以“解空間”的語言描述解決方案,最好能夠以“問題空間”的語言描述解決方案。這是OO得以流行的真正動力,因為OO宣稱自己能夠更好的描述“真實世界”。
注意我要區(qū)分的幾個概念:“解決困境的努力”、“困境的根本原因”、“OO所宣稱的目標”、“OO實際達到的效果”。因為在以往的OO的宣傳中,這些概念是一個有機的整體,而卻認為,其中有諸多“斷裂破碎”之處。
面向過程的編程,面對的困境其實相當多,最根本的原因前面也已經(jīng)指出了。但是在當時,在具體的項目中,在特定的人看來,他們碰的,是各自不同的問題。在人工智能領(lǐng)域,在圖形化界面領(lǐng)域,面對的是模擬的問題。在企業(yè)應用領(lǐng)域,面對的是數(shù)據(jù)訪問與保護的問題。從共同的困境來看,適應變更,方便重用,系統(tǒng)健壯之類的要求,也是需要考慮的。
2、概念的發(fā)展歷程
首先聲明,這是一個假想的歷程,并非真實的歷史。真實的歷史,可以參考以下URL中的介紹:
http://www.student.chula.ac.th/~46829016/history.htm
http://heim.ifi.uio.no/~kristen/FORSKNINGSDOK_MAPPE/F_OO_start.html
http://tech.csai.cn/uml/rjkf.htm
模擬:模擬的概念由來已久,但是如何模擬卻是一個大問題。
抽象數(shù)據(jù)類型(ADT):在對結(jié)構(gòu)(structure)進行簡單的擴展之后,ADT就順理成章的出現(xiàn)了。
封裝:對于ADT的理論總結(jié)可以表述為“封裝帶來了數(shù)據(jù)的安全性”。
繼承:一堆不能重用的代碼是“罪惡”的。繼承首先出來試圖解決這個問題。
多態(tài):是一個意外的收獲,如果各個對象都提供同一個名字的方法,似乎就可以不去管他們的區(qū)別。
在這些努力與嘗試之后,面向?qū)ο髾M空出世,從哲學的高度總結(jié)了這些努力:“你們都是正確努力的一部分,你們都在試圖更好的描述真實世界,真實的世界里一切都是對象,所有的對象在一個分類系統(tǒng)里各安其位,對象之間通過消息相互‘招呼’。應用OO思想,描述真實世界的運行,就是編程的主要工作。”
但是事實上呢?編程并不是描述真實世界!而是描述需求世界。不同的需求世界,需要不同的“世界觀”。這一點,面向?qū)ο蟛]有考慮到。當時流行的思想是通用編程語言,使用一種語言解決世界上所有的開發(fā)難題。而要整體解決各不相同的開發(fā)難題,只能將目光投向“真實世界”,那是各個差異巨大的“問題空間”的唯一一致的背景。
四、面向?qū)ο蟮恼軐W破綻
在此特別感謝徐昊,這一部分該如何寫,我始終沒有想好,在與他討論之后,我基本理出了一個思路。
面向?qū)ο笥袃蓚€哲學基礎,原子論與形而上學。這兩大基礎,在哲學的發(fā)展歷程中,曾經(jīng)如日中天,無可置疑(在古希臘那時候),如果說這樣的哲學不偉大,那就是我太狂妄了,但是如果有人說:“西方哲學在之后的幾千年里沒有進步,古希臘哲學就是西方哲學的頂點,因此面向?qū)ο罄硭斎坏膽摻⒂谶@兩大哲學之上!”你會相信嗎?
1、原子論
西方哲學的發(fā)展,經(jīng)歷了兩次變革,一次是認識論轉(zhuǎn)向;一次是語言轉(zhuǎn)向;第一次轉(zhuǎn)向使哲學的基礎從本體論和形而上學變?yōu)檎J識論,從研究超驗的存在物轉(zhuǎn)到研究認識的主體和主客體關(guān)系;第二次轉(zhuǎn)向把對主客體的關(guān)系的研究變成了對主體間的交流和傳達問題的研究。把對主體的研究從觀念和思想的領(lǐng)域轉(zhuǎn)到了語言的領(lǐng)域(語句及其意義);這兩次轉(zhuǎn)向的代表人物分別是笛卡爾和維特跟斯坦。
————《OO, OO以后, 及其極限》waterbird
看來我可能是比較淺陋了,在我看了waterbird的《OO, OO以后, 及其極限》之后,曾經(jīng)深深的自責過??磥鞳O沒有我想的那么土,不是直接來自古希臘哲學,而是從維特根斯坦繼承而來的。waterbird的引用而后總結(jié)的一段維特根斯坦的話,使我對維特根斯坦大為佩服。
小結(jié)2: 2 主要說明 --- 事實(facts)由原子事實(atomic facts)所組成;原子事實(atomic facts)由更基本的對象(objects)所組成;我們的關(guān)于外部世界的主觀描述圖畫,與它所描述的外部世界具有相同的邏輯結(jié)構(gòu);注: (這即是相當于軟件開發(fā)中的"建模")
還好,在我昨天列出了閱讀書目之后,gigix提醒我看了另外一篇文章:《維特根斯坦早期思想及其轉(zhuǎn)變》,這是一個正兒八經(jīng)的哲學家的文章,總算讓我見識到了軟件開發(fā)這個行當里,頗有些不懂哲學的家伙,拿著哲學來唬人的。
原子事實是最簡單的事實,無法再從中分析出其他事實,分析的結(jié)果只能是對象。因此,原子事實是對象的結(jié)合或配置。“對象是簡單的”〔2.02〕,不可再加以分析,所以,對象就是簡單對象,不過,為清楚起見,維特根斯坦還是經(jīng)常采用“簡單對象”這個說法。簡單對象這個概念引起很多困惑和爭論,其實維特根斯坦自己也很猶豫,他在筆記中寫道:“我們的困難是,我們總說到簡單對象,卻舉不出一個實例來。” 他曾考慮過關(guān)系、性質(zhì)、視域上的小片、物理學里的物質(zhì)點。他還說個體如蘇格拉底、這本書等,“恰恰起著簡單對象的作用”。一條可能的思路是把簡單對象理解為一種邏輯要求,一個邏輯終點:“簡單對象的存在是一種先天的邏輯的必然性。” 在《邏輯哲學論》中,維特根斯坦大致采用了這條路線,這本書里從未舉例說明什么是簡單對象。
維特根斯坦說的對象,是OO中的對象嗎?一個正兒八經(jīng)的現(xiàn)代哲學家的困惑,OO大師們考慮到了嗎?只有樸素、甚至可以說是幼稚的原子論觀點,才會輕松的混淆:事實、原子事實、對象和具體的物質(zhì),物體。對于OO來說,對象非常容易被發(fā)現(xiàn),幾乎隨手一指,都能點到一個對象。
從語言哲學來說,最為困難的是:“有沒有一種語言,可以清晰地、完整地描述這個世界?”邏輯原子論原本認為有可能。但是,維特根斯坦的后期哲學轉(zhuǎn)向,恰恰指出了一個困境,而這個困境即時是人類歷史上最為天才的頭腦,也無法“走出”的。德國人最具有理性的思維能力,而其中最為天才的頭腦卻碰上了理性思維的天花板。維特根斯坦很難理解,越是德國天才,他的語言越是晦澀。倒是從中國哲學的角度,往往能夠看透其中的掙扎。老子在幾千年前就說:“道可道,非常道,名可名,非常名”。因為試圖準確、無誤、無失漏的命名、描述世界的努力,是不可能成功的。
因此,我現(xiàn)在可以斷言,面向?qū)ο蟊澈蟮脑诱摚贿^是直接師承古希臘哲學的簡單、樸素、幼稚的原子論,這樣的原子論哲學,早已破產(chǎn)。作為哲學史的研究對象,自有其價值,而作為指導軟件開發(fā)那么現(xiàn)代活動的哲學理論,實在是太不適用了。
!——面向?qū)ο蟮恼軐W破綻(2)
2、形而上學
當我寫下這個標題的時候,內(nèi)心無比惶恐。這么大個題目,是我這個半路出家,Google成才的家伙能夠談論的嗎?多少哲學家一輩子“皓首窮經(jīng)”,也不過就是研究個形而上學啊。
當初,維特根斯坦去找羅素,問到:“你看我是不是一個十足的白癡?”羅素不知他為什么這樣問,維特根斯坦說:“如果我是,我就去當一個飛艇駕駛員,但如果我不是,我將成為一個哲學家”??梢娬軐W這東西,只有真正的天才才有能力去研究它。
還好,我并不是要研究形而上學,我只是要研究面向?qū)ο蟊澈蟮男味蠈W哲學基礎。
我也不是要證實這個哲學基礎的正確性與適用性。我只需要證明“面向?qū)ο蟊澈蟮哪莻€形而上學基礎是不正確的、是不適用于軟件開發(fā)的。”
面向?qū)ο蟮膬纱蠛诵母拍钍牵?#8220;對象”與“類”。“一切皆是對象”是由樸素原子論而來的。“萬物皆有類屬”就是由亞里斯多德的形而上學來的。
對于亞里斯多德的形而上學理論不熟悉的朋友,可以即時補課,中國人民大學哲學系的《西方哲學史》有好幾節(jié)專門講這個方面:《亞里斯多德的實體論I》、《亞里斯多德的實體論III》。還有就是到Google上去專門搜一下亞里斯多德的邏輯學說,看完以后,咱們回來接著說。
咱們用自己的話說一下:“種”、“屬”、“屬差”以及“定義”這幾個概念。
種:是一個大的概念,假設已經(jīng)預先定義好了的。
屬:所有屬于某一種的概念,都是那一種下面的屬。
屬差:同屬一種的、同一級別的屬之間的差別,或者說個性。
定義:通過種加屬差,可以定義一個屬的概念。
舉例說明:人是二足直立動物。人是一個需要被定義的屬,動物是人之所屬的種,二足直立是人作為動物的共性之外,擁有的個性,也就是屬差。
懂得初步的面向?qū)ο缶幊痰耐緜儯銈兌伎闯鰜砹税?,大多?shù)OO語言也是這么定義類的。你定義一個Animal,再用Person去繼承Animal。在Animal里有一些屬性與方法,在Person里再加一些人所特有的。很多很多的面向?qū)ο蟮慕炭茣?,甚至就是直接用這個定義來舉的例子。
問題出在哪里?或者有人會問:“這樣有什么不對嗎?”
我們可以通過“種+屬差”來定義一個新的屬嗎?定義成立的前提是什么?先要有種的定義。然后才可能有屬的定義。種的定義又是哪里來的呢?在一個種的概念之上,必然存在一個更普遍的種,一個更大的范疇。在亞里斯多德來說,在所有的種之上的種是“存在”,而存在是無法被定義的。而在面向?qū)ο蟮恼軐W里,即使是這一個最基本的哲學困境也被忽略了,無法被定義的概念,被代換為無需由程序員定義的概念(Object)。屬差的區(qū)別在哲學家看來,是本質(zhì)的,是基于深刻認識才能提出的。而在面向?qū)ο蟮恼軐W里,種的共性就是基類所定義的“屬性與方法”,而屬的個性,就是對于基類的擴展。“種+屬差”變成了“公用代碼+擴展代碼”。
當概念定義這樣一個“問題域的描述手段”,演變成“減少重復代碼原則”之后。Class繼承的概念就越發(fā)的模糊不清了。我們來總結(jié)一下:
1、面向?qū)ο笤韭暦Q的描述真實世界的目標,采用的工具卻是樸素的“種加屬差”的方式。
2、面向?qū)ο蠓治鲋?,發(fā)現(xiàn)具體的對象還算是容易的,發(fā)現(xiàn)“種”的概念卻是困難的。
3、在實際應用中,種概念的發(fā)現(xiàn)與定義,被偷換為公共代碼的抽取。
4、由于基類的定義的隨意性,導致子類不但可以擴展基類的行為與特性,還可以覆蓋(改變)基類的行為與特性。
5、由于哲學概念的與開發(fā)概念的混淆,使得在OO領(lǐng)域IS-A、Has-A、Like-A成為最為繞人的概念。
!——面向?qū)ο蟮陌l(fā)展歷程
在寫完了哲學分析部分之后,我總算是喘了一口氣,仿佛穿越了最幽暗的深谷,終于走出了自己最不擅長的領(lǐng)域了。
后來在MSN上和曹曉鋼聊了挺長時間,對于OO的批判,他認為有點過頭了。經(jīng)過我的解釋,他提出了一個更好的建議,清楚的說明自己批判的OO,究竟是哪一個階段的OO,然后才不至于誤傷到已經(jīng)改善過后的OO。所以我打算整理一下對于OO發(fā)展階段的看法,寫在下面:
1、面向?qū)ο蟮恼Z言:先有語言
2、面向?qū)ο蟮姆治雠c設計理論:再有理論
3、面向?qū)ο蟮脑O計原則的全面總結(jié):再有原則
4、設計模式的初步提出:然后才有了真實的經(jīng)驗總結(jié)
5、重構(gòu)方法的提出:然后才考慮到代碼設計的細節(jié)上的改善
6、AOP概念的提出:打破OO封裝的“封印”
7、新語言的出現(xiàn):Python、Ruby之類面向?qū)ο蟮膭討B(tài)語言:更加方便的語言?
8、ASM、CGLIB、Mixin之類技術(shù)的出現(xiàn):OO喪鐘的先聲
具體的對于各個階段的分析,將在隨后展開,目前對于OO的哲學分析,基本上是針對原始的OO概念的。隨后的OO技術(shù)的發(fā)展,也在試圖解決由于OO的哲學基礎假設帶來的問題,當然,越是解決問題,也就離OO的本意越遠,現(xiàn)在有人還以為OO在不斷發(fā)展,而事實上,OO早就盛極而衰,目前已經(jīng)處在破產(chǎn)的前夜了,我的這篇文章,就是打算使這一天,早日到來!
敲響OO時代的喪鐘!——炮彈外面的糖衣
在javaeye與ajoo結(jié)結(jié)實實的吵了一下。是關(guān)于OO的定義的。
我的總結(jié)陳詞如下:
ajoo 寫道: 引用: 面向?qū)ο蟊澈蟮哪莻€形而上學基礎是不正確的、是不適用于軟件開發(fā)的
這句話難道不隱含著你知道對什么是“面向?qū)ο?#8221;這個前提?
請問,如果你沒有一個面向?qū)ο蟮亩x,那么上面的這個"背后的形而上"是什么意思?
既然沒有定義,那么怎么會存在"背后的形而上學"?以至于你需要證明它不正確?
要證明一個沒有定義的東西不正確,這只能是一個玩笑。
undefined就是undefined,根本無所謂正確不正確,也談不上背后的形而上學。
這句話并不隱含著對于“面向?qū)ο?#8221;的定義。
某種需要被明確指出的哲學作為一種推動的力量,導致OO的出現(xiàn)。
導致OO這個時代的到來。
要分析OO時代背后的哲學推動力量,需要某種思想史意義上的猜測,這個猜測不是以對于OO的嚴謹定義為前提的。
可以稱之為:“復雜現(xiàn)象背后的哲學動力猜測”,對于這樣一個OO時代中出現(xiàn)的復雜現(xiàn)象,我們需要有一個“刀刀見血”的“現(xiàn)象批判”!而不是“勉為其難”給OO一個定義。
ajoo 寫道: 老莊別急.研究問題嘛.
你如果這么批判我當然沒異議.
不過你的文章中不是這樣的呀.你根本沒提概念混亂不清這碼事.而是上來就直接給"面向?qū)ο?和哲學拉組織關(guān)系了.
姑且說一個OO的定義:
OO是一種哲學:這是OO所屬種的定義
OO是一種指導軟件開發(fā)的哲學:這是種加屬差的標準定義。
OO作為一種哲學自然包括世界觀和方法論。
OO的世界觀是:“軟件開發(fā)面對的世界中一切皆是對象”;
OO的方法論是:“將世界分解為具有類屬關(guān)系的大大小小的類,是軟件開發(fā)的分析、設計與編程的指導方法”
OOP就是在這種哲學指導之下的開發(fā)方法
OOA就是在這種哲學指導之下的分析方法
OOD就是在這種哲學指導之下的設計方法
總結(jié)而言,這樣的定義實際上毫無疑義,不過是炮彈外面的糖衣而已。無論是對于我還是我的讀者,這樣的定義都沒有給我和他們帶來任何有價值的東西!
ajoo,你覺得這樣的定義有意思嗎?
敲響OO時代的喪鐘!——類型系統(tǒng)
多謝Trustno1的幫忙,他因為忍不住了,所以打算幫我把題破了
寫文章,不破不立。我說這句話還不針對你即將要發(fā)表的解決方案,只是針對你前5頁的"破",本來以為你能說清楚的,但是你破了5頁都破不了,看著肚子都癢,索性就用EL這樣前人的觀點幫你破了,你就快點立論吧。
而他引用的兩段話,一段正好可以用來承上“Elminster的那段關(guān)于面向?qū)ο笈c本體論的論述”,另外一段正好可以用來啟下“事實上,我們猜想是,如果沒有知識表示和自動推力工作的幫助,這些問題(指類,繼承)是無法僅僅通過計算機語言設計的方式來處理的。”——SICP 2.5,中文版136頁,角注118。
Elminster那篇論述,正好和我的文章形成一個互補關(guān)系,他以極為清晰的表達語言,說明了OO打算以類型化方式描述真實世界,所面臨的難題。這也使我不必再次動腦子思考如何回答JavaCup的哲學方面的疑問了。而下面這一段話我想特別再次引用一下:
就我個人來說,比較傾向于認為這條最終是走不通的死路,人是從事物的特征與屬性歸納出它的“類型”,而不是相反。某種意義上說,“類型”是為了節(jié)省描述時間而產(chǎn)生的 …… 唔,這個太遠了,所以就此打住。
大家記住這段話中的,特征、屬性、類型這幾個關(guān)鍵字。我先繞個小彎再回到這個話題上來。
我之前分析的面向?qū)ο蟮恼軐W漏洞時,也有不少朋友認為,說:“面向?qū)ο蟛荒芎芎玫拿枋稣鎸嵤澜?,并非一個有意義的指控。OOA、OOD本來就是用來對需求建模的。也就是打算描述需求世界。”
其實我的指控分為兩個階段,一方面,OO所依據(jù)的哲學導致了軟件開發(fā)的苦難,而且至今余毒未清。另一方面,即使是指打算對需求建模,OO的技術(shù)手段也是有缺陷的。
就這么說吧:OO的類型系統(tǒng),原本是從ADT來的。一個抽象數(shù)據(jù)類型,將數(shù)據(jù)與操作封裝在一起,是出于對于數(shù)據(jù)被“莫名其妙的修改”的擔心。但是,結(jié)果呢,一個ADT如果不支持繼承,那么它的代碼就無法被重用。所以OO就在ADT的基礎上增加的一個繼承,通過繼承的方式來復用以有的代碼。這樣的思路原本沒有太大的問題,如果它僅僅只想表達各種自定義數(shù)據(jù)類型的話。
但是在OO的哲學提出之后,一切皆是對象,所以一切出于類型體系之內(nèi),對于數(shù)據(jù)類型的定義,擴展到了對于現(xiàn)實世界中各種實體的類型定義,整個一個類型系統(tǒng),其內(nèi)在的語義大大擴展復雜化了。更糟糕的是——引用Elminster的話是從事物的特征與屬性歸納出它的“類型”——而因為OO封裝也就是隱藏了內(nèi)部數(shù)據(jù),事物的特征與屬性,從其本質(zhì)屬性,被轉(zhuǎn)義為對外的提供的操作接口。但是,要分析一個實體的本質(zhì),而不是實體的外部表現(xiàn),更不僅僅是“我能對他做什么”。這才是實體分析有可能成功的關(guān)鍵,而在OO的語言設定中,這卻是難以做到的。
我們來看兩張圖片
這是在SICP里討論類型系統(tǒng)的第一張圖片,我稱之為“OO成功案例”。
這是在SICP里討論類型系統(tǒng)的第二張圖片,我稱之為“OO失敗案例”。
為什么一個能夠成功,而另一個卻會失???以往的解釋其實比較“直覺”??粗@個圖,就想當然的以為:“這是因為多重繼承導致的!”事實上呢?
第一張圖中所顯示的成功,很多人會認為這是由于這一個對象塔中的每一個對象都能夠支持加減乘除運算。而在幾何圖形中,這樣一致的操作接口不存在了。而事實上,正是因為復數(shù)、實數(shù)、有理數(shù)、整數(shù),在本質(zhì)屬性上有一致之處,他們才能表現(xiàn)出一致的“可加減乘除性”。而不是相反。當我們畫出第二張對象關(guān)系圖的時候,也不是根據(jù)幾何圖形可以接受的操作類型來進行分類與顯示繼承關(guān)系的,而是根據(jù)不同的幾何圖形的本質(zhì)屬性的相近程度來劃分類型體系的。多邊形的本質(zhì)是: “多條有限長直線組成了一個封閉圖形”,而三角形與四邊形的本質(zhì)則是,邊的數(shù)量分別為三和四。等腰三角形的本質(zhì)是,不但邊的數(shù)量為三,而且其中有兩條邊的長度相等,直角三角形的本質(zhì)是不但邊的數(shù)量為三,而且其中有一個直角。如此等等......
各位,請再次思考這樣的分類體系的內(nèi)涵。
我的結(jié)論是:“一個類型,是由其本質(zhì)決定了所能表現(xiàn)出的可操作性,而不是有其所能接受的操作決定了其本質(zhì)。然而,OO正好把這個問題搞反了!”
敲響OO時代的喪鐘!——繼承、重用、多態(tài)
OO的核心思想就是繼承,那么,OO為什么要繼承呢?對于這個問題,OO的理論大師們有好多個版本的解釋:
1、“這是OO的類型系統(tǒng)自然的要求。設想一下生物學的分類系統(tǒng):動物——>哺乳動物——>靈長類動物——>人類?;蛘咴O想一下我們的概念系統(tǒng):機器——>交通工具——>汽車——>小轎車。這樣的現(xiàn)象在你的生活中難道不是隨處可見嗎?”
2、“如果你有一個類,叫做車輛,這個車輛類能夠移動,現(xiàn)在你要建立一個子類,叫做家庭型轎車,你就可以直接繼承車輛這個類,而不需從頭再寫移動部分的代碼了呀!”
3、“如果你有三個類,三角形、四邊形、正方形,他們都需要顯示在屏幕上,那么你就可以建立一個基類叫多邊形,提供一個draw()方法,然后讓那個三個類都繼承這個多邊形類并且覆蓋那個draw()方法,這樣,你就可以在繪圖的時候,統(tǒng)一對一種多邊形類進行操作,不用去管那個對象究竟是哪一種多邊形。”
這三種解釋,是最為典型的OO繼承的好處的解釋了。但是你如果仔細的看,就能發(fā)現(xiàn)這三種好處,分別描述的是:“概念的特化”、“代碼的重用”以及“接口的重用”。或者可以分別命名為:“繼承”、“重用”、“多態(tài)”。
“這樣有什么問題嗎?”,也許有人會問。問題就出在這三個好處是用一種方法提供,而這三個好處相互之間有時是相通的,有時又是矛盾的!當我們運用OO語言,來寫這樣的繼承的語句時,一切都是“攪和在一起的”!
假設Class A有兩個屬性和兩個方法:String a1;int i;void f1();void f2();當我們另外寫一個Class B去繼承Class A的時候,我們可以繼續(xù)使用某些屬性,而覆蓋另一些屬性,還可以繼續(xù)使用某些方法,而重寫另一些方法。還可以添加一些新的屬性,還可以添加一些新的方法。如果在加上各種訪問控制符的限定與修正。誰能夠告訴我:“這個Class B究竟想干什么?!”
也許有人會繼續(xù)為這樣的現(xiàn)象辯解:“這是對于繼承的誤用,正確的OO程序不會這樣的!”
但是,這個辯解并不成立,幾乎所有的OO的編程語言,都沒有在繼承問題上做出太多“非此即彼”的限制,原因只有一個,那就是,在某些特定的場合,這樣的“拼盤”是相對最合理的編碼方式。
我們前面還沒有提到多重繼承,一個允許多重繼承的語言,會讓這個問題更為復雜,也可以說會使得場面越發(fā)的混亂。讓我們舉一個例子,這是Eiffel語言的繼承語法,讓我們看一看面對繼承這樣一件事情,一個程序員,究竟需要考慮多少問題。來源是《對象揭密》,我就一邊抄,一邊直接翻成中文了。
繼承 :inherit 父類列表
父類列表 :{父類 ";" ... }
父類 :類名[特性適配說明]
特性適配說明:[Rename] :重命名以消除名字沖突
[New_exports]?。褐匦略O定特性導出的分組
[Undefine] ?。撼蜂N定義
[Redefine] ?。褐囟x以取代虛函數(shù)
[Select] :更加高級的功能
end
最值得看的就是這個特性適配說明,更加深入的說明還是各位自己去找書看吧。這就是號稱優(yōu)雅的解決了OO繼承問題的Eiffel語言。他所謂的優(yōu)雅,可以不客氣的說,就是把所有的麻煩都明確的告訴你,而不是像C++和Java那樣,假裝什么事情都沒有發(fā)生過。但是,麻煩依然在那里,并沒有減少,根本的解決方法,是應該不讓這樣的麻煩出現(xiàn)呀!可是OO確實無法做到這一點。
因為他把數(shù)據(jù)和操作封裝在了一起,然后又偷換了實體本質(zhì)的概念,在這樣的情況下的OO,他的繼承是肯定搞不好的!
敲響OO時代的喪鐘——接口、泛型與重用(1)
先說點提外話,我從小學開始學習BASIC和LOGO,到后來學習了FoxBase、FoxPro、C/C++、Visual Basic、VBScript、JavaScript、PHP,之后才開Java編程,之后也沒有再換過語言。誠實的說,只有Java語言,是我認認真真的學習和研究的。對于面向?qū)ο蟮睦斫?,也是在學習和使用Java之后,才真的開始深入思考這方面的問題,在此之前,我甚至認為所有的語言都沒有什么本質(zhì)的差別,面向某某和面向某某之間也沒有什么大不了的差別。
所以當我想要寫這篇文章的時候,其實內(nèi)心是相當惶恐的,我對于面向?qū)ο蟮牧私猓鋵嵵粊碜杂谝环N語言,那就是Java,而Java是不是就等于是面向?qū)ο竽?,只怕是不能這么說的吧。
JavaEye有人留言:“不要到時候說不圓影響了一世英名。”;“討論這個問題,我還是建議去看 SICP,你會發(fā)現(xiàn)所有OO具有的思想SICP都講到了”;“實際上我很懷疑莊某最后還是跑不出SICP的框架,如果是這樣,那么其理論的價值就要打折扣了。”我那個慌啊,趕緊到書店去買了SICP回來仔仔細細的啃,然后再在MSN上向T1好好的請教過幾次,最后總算放心了,我的思路,不在SICP的框架內(nèi),或者說,不在SICP作者的思考局限之內(nèi)。
還有人留言,提到了C++:“OO門檻較高是不爭的事實。的確很多人并沒有進入。有句話可以套
用,沒有三年的功底,最好不要說懂C++。幸運的是這門東西的回報,會告訴你所付出的是完全值得的。”我又慌了,C++背后的面向?qū)ο螅蔚雀呱睿覅s從來沒有用C++做過哪怕1000行以上的程序,這等門檻都沒有入的人,有資格評價面向?qū)ο竽敲创蟮氖虑椋s緊的,我又到書店去買了一本《對象揭秘》,我對于當年gigix的一篇介紹《編程語言的宗教狂熱和十字軍東征》始終記憶猶新,里面提到了面向?qū)ο螅岬搅薈++的無數(shù)缺點,還提到了Eiffel,一個據(jù)說是比C++要好無數(shù)倍的面向?qū)ο蟮恼Z言。如果我要想加快速度,又想保證質(zhì)量的話,從《對象揭秘》里面應該可以找出很多現(xiàn)成的彈藥吧。
抱著急于求成的功利主義目的,我開始仔細看這本《對象揭秘》,一看之下,真是大有收獲:
*C++果然毛病多多,而且作為第一個大面積流行的OO語言,OO的實際含義更多的來自于C++。
*Java的毛病少了很多,因為它引入的一些概念,不再使用的一些概念,大大的減少了C++式OO編程的陷阱,只是這樣一來,在復雜問題上使用Java語言,往往會寫出很丑陋的程序。
*Eiffel同樣也是反思C++缺點的語言,但是它的改進基本上是表面的,Java是使問題簡化,哪怕犧牲語言的表達能力,而Eiffel是使問題表面化、集中化,陷阱雖然沒有了,但是問題一個都沒有減少,反而因為“讓人眼花繚亂的復雜語法”,讓人望而卻步。
*《對象揭秘》是一本很一般的書,作者花了十多年的時間攢出一本書來,實質(zhì)上還是BBS里一段一段討論的水平。
————————————————————————————————————————
好了,題外話結(jié)束,接下來討論正題,今天主要研究OO的概念中兩個較為邊緣的概念“接口”與“泛型”,以及探討一個實際上最為重要的誤用——“重用”。
1、關(guān)于“接口”
接口是什么東西?接口是一個很奇怪的東西!接口之所以奇怪,因為他的來龍去脈實在是讓人看不懂?;A的OO概念中,并沒有接口的位置,因為按照“經(jīng)典的”面向?qū)ο笏季S,一個沒有代碼、沒有內(nèi)部數(shù)據(jù),只有方法說明的東西,是無法理解的。
追根溯源的說,首先是在C++的面向?qū)ο髮嵺`中,發(fā)現(xiàn)了對于“抽象類”的需要,為什么需要抽象類呢?因為代碼重用的需要,比如一個基類,其實就是一堆公用代碼,有一個名字把它框起來叫一個類,但是完全沒有道理把它實例化。像這種“發(fā)育不完全的類”,該拿它怎么辦呢?OK,就讓它不能“出生到這人世間來”。抽象類的本質(zhì)就是這個樣子的。
到了Java,因為對于多繼承的恐懼,Java完全擯棄了多重繼承,這是Java被攻擊得最多的地方,因為這樣的單根繼承,實在是因噎廢食——因“怕被繼承體系搞亂”廢“更加方便道代碼重用”。于是Java就說了:我們有一個“安全的”多重繼承,叫做“接口”,這個接口,完全沒有代碼,只有說明,所以絕對安全,但是由能夠?qū)崿F(xiàn)多重繼承的好處云云。
而事實上呢?多重繼承的根本目的,并不是像Java所宣稱的那樣為了“同時繼承多種類型”,而是為了“同時重用多組代碼”。接口這一發(fā)明,完全不能達到多重繼承的代碼重用效果,卻被宣稱為多重繼承的替代品。其實質(zhì)是:“從一個發(fā)育不完全的實體,變成了一張徹底沒有發(fā)育的皮”。
最為令人感到奇怪的,還不是“接口的出現(xiàn)”,而是“面向接口編程”的出現(xiàn),Java被冠以“面向接口的語言”的美名,“面向接口設計”成了OO的設計原則之一,“針對抽象,不要針對具體”,成了OO名言之一。實在是......
關(guān)于OO的設計原則,我下面還會專門討論,這里先指出一個大漏洞:“抽象類的那個抽象,和與具體相對的那個抽象,根本就不是一回事!”
寫技術(shù)文章,例子其實很難舉,特別是找到有殺傷力的,決定性的例子,相當困難。昨天我接著看《對象揭密》,總算被我找到一個,當然,它那上面的解說,實在是比較模糊,因此我決定用自己的話重新敘述一遍,代碼示例用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函數(shù)。當然,這只是舉例,實際上這三個add中,有兩個是不合理的。
java代碼:
ArrayList<X> listx=new ArrayList<X>();
ArrayList<Y1> listy1=new ArrayList<Y1>();
ArrayList<Y2> listy2=new ArrayList<Y2>();
listx.add(new X());
listx.add(new Y1());
listx.add(new Y2());
listy1.add(new Y1());
listy2.add(new Y2());
這幾行代碼都非常簡單,只是為了說明一個道理,在ArrayList<Y1>和ArrayList<Y2>中,能夠放的就只有Y1和Y2了,而在以X為泛型的ArrayList中,就可以放X、Y1、Y2了。而當然了,這樣的用法,只怕是不合泛型的目標的,本來就是希望能有一個類型的自動檢查與轉(zhuǎn)換,都放在ArrayList<X>中,幾乎就等于都放在ArrayList<Object>中了。
現(xiàn)在我們有這樣一個需求,對于得到的ArryaList,能夠一一調(diào)用里面的對象的add(int a)方法,當然了,只要這個ArrayList里的對象都是X或者X的子類就行了。我們可以寫出這樣的代碼:
java代碼:
public void addListX(ArrayList<X> listx){
for(int i=0;i<listx.size();i++){
X x=listx.get(i);
x.add(1);
}
}
是不是很簡單?且慢,這個addListX函數(shù),我們能夠把listx傳遞給它,但是能不能把listy1和listy2
也傳遞給它呢?如果我們能夠把listy1和listy2傳遞給它,就相當于執(zhí)行了如下的類型轉(zhuǎn)換代碼:
java代碼:
ArrayList<Y1> listy1=new ArrayList<Y1>();
ArrayList<X> listx=listy1;
這樣做行不行呢?在Java和C++中,是不行的。也就是說,如果我們要想只寫一遍addListX這樣的函數(shù),而不用再多寫兩遍 addListY1();addListY2();這樣的函數(shù),就需要把所有的X,Y1,Y2這樣的類型都放到ArrayList<X>這樣的容器里,否則,addListX函數(shù),是不接受ArrayList<Y1>和ArrayList<Y2>類型的。即使Y1和 Y2是X的子類型,ArrayList<X>與ArrayList<Y1>也毫不相干。不能相互轉(zhuǎn)換。
有人也許會說,為什么這么限制嚴格呢?Eiffel就沒有這么這么嚴格的限制,他允許ArrayList<Y1>自動轉(zhuǎn)型為 ArrayList<X>,這樣是好事情嗎?如果listy能夠被轉(zhuǎn)型為ArrayList<X>,那么就可以往里面添加Y2類型的對象了,這又是原來的泛型ArrayList<Y1>不允許的。也就是說:除非addListX能夠保證只對listy1做只讀操作,否則,類型安全性這個泛型原本要追求的目標就不能實現(xiàn)了。而如果要追求絕對的類型安全性,像C++和Java那樣,那么代碼要么就得寫三遍,要么X、Y1、 Y2類型的對象就得都放到ArrayList<X>這樣的泛型容器里去。
注意看這其中的左右為難的狀況,繼承、多態(tài)、泛型,并不是真正正交的、互不干擾的,而是在一個相當普通的目標面前,他們就起了沖突了。
敲響OO時代的喪鐘——重用為什么那么難?
先說句提外話,由于徐昊的推薦,我在網(wǎng)上找到了《Modern C++ Design》繁體中文版的前四章PDF文件。果然不出我所料,Loki的設計思路與我的隨后將會介紹的自己的設計實現(xiàn),實有異曲同工之妙。對于C++的熟悉程度超過Java的某同學,可以先去看看這本書,如果能夠同意書中的觀點,再來與我討論,相信會得到更多的收獲。
程序員都是聰明人,沒有誰愿意干重復勞動這樣的傻事,因此,程序中出現(xiàn)重復代碼是程序員的恥辱。就算不能消除重復代碼,至少也可以對于相同的功能,用不同的代碼來實現(xiàn)所以發(fā)明新輪子的程序員才會那么多。
面向?qū)ο笞鳛橐环N橫空出世的新技術(shù),首先承諾的就是“更好的重用性”,而“重用性”這樣一個閃閃發(fā)光的詞,也的確能夠吸引程序員的實現(xiàn),那么多新的理論、新的技術(shù)、新的方法、新的框架、新的思想,用來說服別人接受的一個最大的理由,就是“更好的重用性”。然而,OO以及一直以來不斷發(fā)展的 OO相關(guān)技術(shù),對于重用性的提高,作出了多大的貢獻呢?
JavaEye的age0有一段話特別讓我佩服:“我還是得反復強調(diào),OO設計的價值并不在于所謂的“代碼重用”或者“接口重用”,任何一種設計方法都能夠獲得“重用”的能力,就算是寫匯編一樣可以“重用”。”一個同志能夠如此決絕的對于重用不屑一顧,真是了不起。我們還是來面向大多數(shù)希望更好的重用的程序員,分析一下在OO出現(xiàn)之后程序員是如何追求重用這一目標的吧。
在面向過程的時代,重用很簡單,就是代碼的重用,函數(shù)、函數(shù)庫、模板、模板庫。如此而已。在ADT出現(xiàn)之后,世界分裂了。代碼重用的需求,現(xiàn)在分裂為三個部分:數(shù)據(jù)類型的重用、ADT內(nèi)部代碼的重用、操作ADT的代碼的重用。
這句話特別關(guān)鍵,讓我再仔細分析給大家看看:ADT=抽象數(shù)據(jù)類型。就是封裝了操作和數(shù)據(jù)的一種用戶自定義數(shù)據(jù)類型。
1、如果僅僅是ADT,那么相近的用戶自定義數(shù)據(jù)類型就無法重用,因此出現(xiàn)了一個數(shù)據(jù)類型的重用需求;
2、因為ADT封裝了對于數(shù)據(jù)的操作,對于A類數(shù)據(jù)的操作,就無法被B類數(shù)據(jù)重用,因此出現(xiàn)了一個ADT內(nèi)部代碼的重用需求;
3、因為對于ADT的操作是借助ADT的外部界面執(zhí)行的,也就是說,對于接近但是不同的類型的操作,必須寫出不同的代碼,因此出現(xiàn)了對于操作ADT的代碼的重用需求。
這樣的分裂的三個需求,在隨后的OO的發(fā)展歷史中,分別被兩種方法來初步滿足。第一、第二種需求,被繼承這樣的技術(shù)來試圖滿足;第三種技術(shù)被泛型類試圖滿足。這兩個技術(shù)滿足了三種重用的需求了嗎?沒有!不但沒有滿足,而且還帶來的諸多麻煩的問題,更在分別滿足三種需求的時候,起了沖突。(前面已經(jīng)討論過的內(nèi)容,可以回頭再看一看,我將來再改寫這篇文章的時候,會將封裝 VS. 重用性的分析,作為一根主線貫穿OO分析的始終,現(xiàn)在就不重新組織結(jié)構(gòu)了。)
由于封裝與重用性之間,存在著本質(zhì)性的沖突,因此,OO的分析、設計、編程方法就始終處于一個難學、難用、難懂的狀態(tài)。我們說給OO下定義非常困難,但是大家都應該承認,ADT是OO的根。數(shù)據(jù)與操作的封裝是一切OO思想的基礎,也是所有OO信奉者從來沒有懷疑的“前提”!
在繼承與泛型不能解決重用難題之后,OO大師們提出了OO設計原則,提出了OO設計模式,這是我接下來的文章里將要細細批駁的兩大“貢獻”。但是OO的原則、模式,依然沒有解決重用難題。在此之后,又有人提出了AOP、IoC這樣的概念,還有人真正開始和我一樣懷疑封裝的意義,而開發(fā)了 CGLib,Mixin這樣的動態(tài)改變對象行為與結(jié)構(gòu)的技術(shù),這也是我將要批判的“最新進展”。到了這個時候,真正理解OO本質(zhì)的人,應該已經(jīng)看出來了, OO時代即將結(jié)束,因OO而帶來的混亂也該結(jié)束了?,F(xiàn)在唯一的問題是:“什么樣的技術(shù),才是可行的、替代的方案呢?”
敲響OO時代的喪鐘——OO設計原則批判
OO設計原則!
這是很多開發(fā)資源網(wǎng)站必備的一個欄目、專題、至少也要轉(zhuǎn)載一篇放在自己的網(wǎng)站上的東西。所有的程序員,如果你不開發(fā)面向?qū)ο蟮某绦蛞簿土T了—— 反正你已經(jīng)落伍很久了,如果你要想開發(fā)OO程序,而竟然沒有把那些OO設計原則熟讀背誦,搞得滾瓜爛熟。那么你就完了,一個公司面試你的時候,問你:“你對SRP的理解是怎么樣的?”,而你居然不知道SRP是什么,那么這家公司你也就別想進去了。作為OO程序員的《舊約圣經(jīng)》(設計模式自然是《新約圣經(jīng)》)他怎么就會那么神圣呢?
介紹OO設計原則的文章很多,我在google上搜索了一下:“約有58,200項符合OO設計原則的查詢結(jié)果”。真正能夠介紹得透徹的,還真是沒幾個。正好我手邊有一本Bob大叔的《UML for JAVA Programmers》那上面的介紹,在我看來,是最好的OO設計原則介紹之一了。另外一本不在手邊的《敏捷軟件開發(fā) 原則、模式與實踐》也是Bob大叔的,還要詳盡一些。如果要批判,自然要找這樣的靶子來練!
1、單一職責原則(SRP)
一個類只能因為一個原因而改變。
“A class should have one, and only one, reason to change.”
這個原則何等的簡單,又是何等的模糊呢?什么叫做一個原因呢?我們來看下面這個類:
java代碼:
class User{
private String name;
private int age;
public void setName(String name){
this.name=name;
}
public void setAge(int age){
this.age=age;
}
}
請問,這個類是不是違反了SRP原則呢?設置用戶的名字與設置用戶的年齡,是一個原因,還是兩個原因呢?Bob大叔在自己的書里舉了一個例子,說明了違反SRP原則的情況,一個Employee類,包含了計算工資和扣稅金額、在磁盤上讀寫自己、進行XML格式的相互轉(zhuǎn)換、并且能夠打印自己到各種報表。我說拜托啊大叔!一個類里的方法多到如此驚人的程度,自然是違反了SRP原則,但是我們要為它瘦身,該瘦到什么程度呢?按照大叔繼續(xù)給出的自己的答案,它把計算工資和扣稅金額的兩個功能留給了Employee,其他的分離了出去。這個答案正確嗎?員工的工資和稅收是自己算的?還是有一個“財務部”對象來計算的呢?且不說那么掃興的事情,就看看那個類圖里分離出來的那幾個類:
EmployeeXMLConverter、EmployeeDatabase、TaxReport、EmployeeReport、 PayrollReport。這些類還需要有自己的內(nèi)部數(shù)據(jù)嗎?請注意,他們事實上都是通過接受Employee對象的內(nèi)部數(shù)據(jù)而工作的,換句話說,這些所謂的類,根本就不是什么類,只不過是一個個用Class關(guān)鍵字包裹起來的函數(shù)庫!當我們看到一個臃腫的Employee類,被拆成6個各不相同的類之后,內(nèi)心自然升起了“房子打掃干凈之后的喜悅”。但是,且慢!灰塵到哪里去了呢?當我們把一個類拆成6個類之后,那個原本的類自然已經(jīng)遵守了SRP原則,然后新誕生的5個類,是不是也該遵守SRP原則呢?如果我們不能將一個原則應用于整個系統(tǒng)設計中的所有的對象,僅僅像小孩打掃衛(wèi)生一樣,把灰塵掃到隔壁房間,這剩下的事情,誰來處理呢?
好吧,我們不要這么嚴厲,畢竟這只是一個原則,追問太深似乎并不合適。我只想再搞清楚幾個問題:按照SRP原則,C++中是不是一律不應該出現(xiàn)多重繼承呢?按照SPR原則,Java中的一個類是不是一律不應該既繼承一個類,又實現(xiàn)一個對象呢?一個簡單的POJO,被動態(tài)增強之類的辦法,添加出來的新的持久化能力,是不是也是違反SRP原則的呢?歸根結(jié)蒂,我的問題是:按照SPR原則,我那些剩下的,但是又必須要找地方寫的代碼,究竟應該寫在哪里呢?
2、開放-封閉原則(OCP)
軟件實體(類、模塊、方法等)應該允許擴展,不允許修改。
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
這個原則倒是非常的清楚明白,你不能改已經(jīng)寫好的代碼,而應該擴展已有的代碼!如何做到這一點呢?Bob大叔舉了一個經(jīng)典的例子:個人認為這個例子說明的是一個使用接口,隔離相互耦合的類的通常做法。而且這個做法不應叫做OCP,而應該叫做DIP。查了一下c2.com里的OCP的解釋:
In other words, (in an ideal world...) you should never need to change existing code or classes: All new functionality can be added by adding new subclasses and overriding methods, or by reusing existing code through delegation.
但是在Bob大叔的OCP解釋中,這個原則的具體實現(xiàn)被偷換了概念,從“鼓勵多使用繼承”,變成了“鼓勵面向接口編程”。為什么?因為繼承式OCP實踐已經(jīng)被證明會帶來相當多的副作用,而面向接口編程又如何呢?我們在討論DIP的時候再詳細討論吧。
有一個在JavaEye的討論的連接可以參考:對于OCP原則的困惑
3、里斯科夫替換原則(LSP)
子類型必須能夠替代他們的基本類型。
“Subtype must be substitutable for their base types.”
對于這個問題,我都不用多說什么,只引用Bob大叔在c2上的一句話,以作為我的支持。
“I believe that LSP is falsely believed by some few to be a principle of OO design, when really it isn't.”
4、依賴關(guān)系倒置原則(DIP)
A.上層模塊應該不依賴于下層模塊,它們都依賴于抽象。
B.抽象不應該依賴于細節(jié),細節(jié)應該依賴于抽象。
“A. High level modules should not depend upon low level modules. Both should depend upon abstractions. ”
“B. Abstractions should not depend upon details. Details should depend upon abstractions.”
Bob大叔還補充說明了一下,這個原則的意思就又變了:更確切的說,不要依賴易變的具體類。也就是說,不容易變的具體類,還是可以依賴的。那么,當我們開始一次系統(tǒng)開放的時候,那些類是易變的具體類呢?那些類是不易變的具體類呢?怎么才算是易變、怎么才算是不易變呢?我們來看看代碼吧:
java代碼:
class A{
public void doA(){
}
}
class B{
A a=new A();
a.doA();
}
按照DIP原則,Class B依賴于一個具體實現(xiàn)類Class A,因此是可恥的!最起碼你應該寫成這樣:
java代碼:
interface A{
public void doA(){
}
}
class AImpl implements A{
public void doA(){
}
}
class B{
A a=new AImpl();
a.doA();
}
這樣,AImpl和B都依賴于Interface A,很漂亮吧。還不夠好,A a=new AImpl();還是很丑陋的,應該進一步隔離!A a=AFactory.createA();在AFactory里,再寫那些見不得人的new AImpl(); 代碼。然后呢?這還沒完,更加Perfect的辦法,是采用XML配置+動態(tài)IOC裝配來得到一個A,這樣,B就能夠保證只知道這個世界上有一個 Interface A,而不知道這個Interface A是從哪里來的了。這么做的理由是什么呢?有一個很嚇人的理由:“如果A被B直接使用,那么對于A的任何改動,就會影響到B了。這就叫依賴,而這樣的依賴會很危險!”
我們來看看這頗有說服力的話,漏洞何在?A的變化有兩種情況,一種是只修改A中的方法的內(nèi)部實現(xiàn),無論是直接使用A還是使用Interface A的某一個實現(xiàn),這時候B的代碼都不用改。另一種是修改了方法的調(diào)用接口,如果是直接使用A的Class B,就需要修改相關(guān)的調(diào)用代碼,而如果是使用接口的呢?就需要同時修改Class B和Interface A。這樣看來,采用接口方式,反而需要修改更多的代碼!這使用接口的好處何在?
5、接口隔離原則(ISP)
客戶端不應該依賴于自己不用的方法。
“The dependency of one class to another one should depend on the smallest possible interface.”
這個我就不說了!因為這個原則和SPR原則是矛盾的!就像合成復用原則(CRP)與LSP原則矛盾一樣。
關(guān)于這個批判,我昨天晚上只寫了一半,今天算是虎頭蛇尾的寫完了。最后錄一段Bob大叔的話,作為結(jié)尾:
什么時候應該運用這些原則呢?首先要給您一個痛苦的告誡,讓所有系統(tǒng)在任何時候都完全遵循所有原則是不明智的。
運用這些原則的最好方式是有的放矢,而不是主動出擊。在您第一次發(fā)現(xiàn)代碼中有結(jié)構(gòu)性的問題?;蛘叩谝淮我庾R到某個模塊受到另一個模塊的改變的影響時,才應該來看看這些原則中是否有一條或者多條可以用來解決問題。
......
找到得分點的最佳辦法是大量寫單元測試。如果能夠先寫測試,再寫要測試的代碼,效果會更好。
讓我來翻譯一下上面的告誡。原則不是你可以用來預防問題的,而是當你已經(jīng)陷入麻煩的時候,你可以停下來悔恨一下。至于解決之道,依然不是很清楚,因此,你需要寫大量的單元測試。而且,大量的單元測試并不是幫你檢查你的設計漏洞,而是幫你更真切的感受自己的設計是否正確。至于他究竟是不是正確,這就看個人自己的感覺了。更為驚人的是,在測試驅(qū)動開發(fā)的建議中,如何驅(qū)動開發(fā)的準則,竟然是循環(huán)的來自于OO設計原則的。
這樣的OO設計原則,就像老爸老媽給我們的人生教誨:“你要做好人啊”,別的什么都沒說。而且我們還遇到了話都說不清的糊涂爹娘,怎么才算好人,不清楚,怎么才算壞人呢?被警察抓了,肯定就是壞人了。至于如何才能做得更好?自己體會吧。
敲響OO時代的喪鐘——設計模式批判(1)
為什么要批判設計模式?設計模式不是OO開發(fā)的救星嗎?作為“可復用的面向?qū)ο?#8221;的基礎設施,設計模式大大的超越了OO設計原則給予我們的承諾,還記得我們前面的分析嗎?OO設計原則并不擔保你在設計之前就能避免錯誤,相反的,你往往需要在屢屢受傷之后,才會明白設計原則的真諦。而設計模式是如此的偉大,他甚至可以幫你提前避免問題,只要你可能遇到的問題,符合設計模式手冊中,所描述的那種場景,基本上你就可以直接采用相應的設計方案了。如果找不到正好合適的,你也可以改造自己面對的問題,使得他看起來究就像設計模式手冊中描述的那樣,然后你就可以放心使用相應的設計方案了。如果你無法在那23個模式中找到合適的答案——你可真是太挑剔了——那么你只能自己想法組合一下那23個中的2~3模式,總之,一切盡在其中
好吧,事實其實沒有那么夸張,“GoF”從來沒有宣稱“設計模式”能夠包治百病,更沒有說過使用“設計模式”可以預防疾病,他們也的確謙虛的承認,設計模式肯定不止23個。但是,GoF也必須承認的一點就是:“Design Patterns原本是用來指導設計的。大多數(shù)時候,都是在實際開發(fā)之前完成的。”而且,按照設計模式原本的思維模式,應該把一個系統(tǒng)中的各個類,按照他們所說的一堆模式組織起來,其根本目的,就是不讓后來的改動,再去修改以前的代碼,所謂OCP原則在設計模式中的實際體現(xiàn),就是對擴展開放、對修改封閉。
In other words, (in an ideal world...) you should never need to change existing code or classes: All new functionality can be added by adding new subclasses and overriding methods, or by reusing existing code through delegation.
再強調(diào)一遍:“設計模式認為,靈活性、可復用性是設計出來的”,而在此之后的發(fā)展我們可以看到,新的大師們又偷換了概念,將“設計——實施”的兩階段過程,變成了一個“TDD+重構(gòu)”的持續(xù)改進過程,他們不但提倡改以有的代碼,而且要大改特改,持續(xù)不斷的改,唯一還帶著的帽子是:“重構(gòu)的目標是得到設計模式。”重構(gòu)真的能以設計模式為目標嗎?我們下一篇再討論。
請允許我先借力打力,利用重構(gòu)這一新生事物,攻擊一下設計模式這個老東西。為什么靈活性、可復用性不能是設計出來的?
軟件開發(fā),一個很重要的工作,就是對付“需求變更”,軟件工程的辦法是盡可能的抵擋、或者有效控制變更的發(fā)生。技術(shù)人員的目標,則是努力開發(fā)出更加靈活的技術(shù),以適應可能的變化。值得一提的是,現(xiàn)在軟件開發(fā)的管理者,也開始相信,擁抱變化比限制變更,是更為可取的手段了。
更加靈活的技術(shù),更加容易理解,方便描述事實的語言,設計模式等等等等,都是用來適應可能的變化的。對于技術(shù)人員來說,如果能夠預測可能的變化,并在系統(tǒng)設計期就將其考慮進去,那么變化就成為計劃的一部分,注意這句話,因為實際的情況往往是:“計劃趕不上變化”。越是自信的技術(shù)人員,越是自以為能夠預測可能的變化,將變化提前設計進入自己的系統(tǒng)。所以,濫用設計模式的人,往往是那些自以為水平很高的半桶水。而重構(gòu)這一思路的出現(xiàn),就是對于設計模式這種“企圖預測變化”的否定。事實上,即使是重構(gòu),也是危險的,因為從原始狀態(tài),到第一個變化發(fā)生時,我們能夠得到的只有兩個狀態(tài)點,這兩個點聯(lián)成直線所指向的一個方向,并不一定就是變化的方向,因此,重構(gòu)是一個好辦法,而將得到設計模式作為重構(gòu)的目標,卻相當危險。
設計模式背后的思路非常清楚,就是將可能變化納入設計模式的考慮之中,所以我們看到設計模式的目標“可復用”,其實是一個轉(zhuǎn)了一個彎以后的目標,“在盡可能重用已有代碼的前提下,適應變化”。我的觀點是:“首先需要滿足的不是復用,而是快速修改”,這個問題太大以后有機會再討論吧。
這篇關(guān)于設計模式的批判,我寫了好幾天,始終感覺難以下手。今天和徐昊討論,他的話我認為非常有道理:“設計模式的成功,正好證明了OO的失敗”。這個思路相當有用,我決定就按這個調(diào)子來寫。當然,設計模式并不是只有一個,而是有很多很多個,作為一種“專家經(jīng)驗交流的規(guī)范描述格式”,設計模式已經(jīng)非常多了。我們今天也不批判更多的模式,僅僅對GoF的23個模式下手吧。
GoF的23個設計模式,主要分為三類:創(chuàng)建型模式、結(jié)構(gòu)型模式、行為型模式。我們就分別批判這三種模式吧。
創(chuàng)建型模式之所以需要,其實正好證明了OO的失敗。因為在OO的思想中,創(chuàng)建對象這種事情,天然就應該是對象自己處理的。一個類在定義時,可以定義一個以上的構(gòu)造函數(shù),來決定如何得到自己的實例,那為什么還需要把創(chuàng)建對象這個事情,交到別人手里?按照SRP原則,無論出于什么原因,創(chuàng)建自己總是自己的職責吧!所以按照SRP原則,所有的創(chuàng)建型模式都不應該出現(xiàn),出現(xiàn)了也是錯誤的。但是為什么我們需要創(chuàng)建型模式呢?先看看GoF的解釋:“隨著系統(tǒng)演化得越來越依賴于對象復合而不是類繼承,創(chuàng)建型模式變得更為重要。當這種情況發(fā)生時,重心從對一組固定行為的硬編碼,轉(zhuǎn)移為定義一個較小的基本行為集,這些行為可以被組合成任意數(shù)目的復雜的行為,這樣創(chuàng)建有特定行為的對象要求的不僅僅是實例化一個類。”
這樣的解釋,有一定的道理,但是卻局限于“用組合取代繼承”這樣一個“當年的熱門話題”。在我看來,根本的原因在于:“在OO的經(jīng)典思想中,對象之外的環(huán)境是不存在的,因此,所有的對象,都要考慮如何產(chǎn)生自己,如何銷毀自己,如何保存自己,總之,自己的事情自己做。”Java的一個巨大進步就在于,銷毀自己這件事情,不再強求對象自己去考慮了,因為這種事情過于瑣碎,而且復雜易錯。但是創(chuàng)建自己這件事情,java依然沒有考慮到也不該交給對象自己考慮的。于是設計模式中的種種創(chuàng)建模式被發(fā)明出來,用以滿足復雜多變的創(chuàng)建需求。這個根本原因同時也解釋了為什么單例模式會被發(fā)明,按照GoF的解釋,是無法說明為什么我們會需要單例模式地。而當我們有了對象環(huán)境的概念之后,各種創(chuàng)建自然有“容器環(huán)境”來完成,“單例”模式也只需要在環(huán)境中配置,有了OO容器之后,所有的創(chuàng)建模式都被一個概念所代替了。在沒有這樣的概念之前,我們需要用各種模式技巧,來實現(xiàn)“支離破碎”的環(huán)境。而在真正的容器環(huán)境出現(xiàn)之后,我們就不再需要這些設計模式了。
讓我再說一遍:“如果你能夠理解為什么現(xiàn)在會出現(xiàn)那么多容器,就能理解設計模式中的創(chuàng)建模式,只不過是用來解決OO概念欠缺的一種不完善的手段了。”
再來看結(jié)構(gòu)型模式,個人認為將“Adapter、Bridge、Composite、Decorator、Facade、 Flyweight、Proxy”統(tǒng)統(tǒng)歸入結(jié)構(gòu)型模式,是不恰當?shù)?。除了Composite模式算是結(jié)構(gòu)模式,F(xiàn)lyweight算是一種“節(jié)約內(nèi)存的技術(shù)手段”之外,其他的模式,都屬于打破OO靜態(tài)封裝的技巧。我們知道,按照OO的設定,一個類,就是一種類型,它在代碼寫下的時候,就已經(jīng)決定了自己的內(nèi)部數(shù)據(jù)與外部行為。怎么還能在運行的時候再改變呢?事實證明,這樣需求不但存在,而且重要,設計模式之所以被大家欣賞,一個重要的原因,就是他能夠幫助程序員部分擺脫“靜態(tài)封裝屬性與行為”的限制。
敲響OO時代的喪鐘——寫作計劃更改
原本這一篇我是打算寫《設計模式批判(2)》的,但是經(jīng)過權(quán)衡,我決定放棄原本的進度計劃,將攻擊已有技術(shù)成果的文字大大縮短,盡快開始寫我的設計方案部分。因此,設計模式的批判我將改變批判的方式,不是直接空對空的批判,而是在提出了自己的設計方案之后,通過對比,向大家表明,使用新的思維模式與技術(shù)手段,原有的設計模式需要解決的問題,現(xiàn)在能夠得到更加靈活、易懂的解決。
同樣的,對于AOP、CGLIB、Mixin之類的新技術(shù),我也將通過實際的例子證明,在我的新方案內(nèi),這些技術(shù)所帶來的靈活性,同樣能夠自然、安全、方便的得到。
在JavaEye的討論中,JavaCup給了我一堆非常好的地址,我會仔細去看的。初步看下來的結(jié)果是,攻擊OO的語氣似乎比我的還要激烈,而改進的方案基本上還是從LISP這樣的角度出發(fā)的。因此我的版本應該還是有一定的新意的。
關(guān)于JavaCup所同意的age0的觀點,我不敢茍同,因為按照他的邏輯,一切技術(shù)的優(yōu)劣都是不存在的,有區(qū)別的,只是使用者的水平高低不同。而且,似乎firebody也持類似的觀點。一種技術(shù)如果你用得不好,就該自己反??!那么,為什么我就不能也假設“OO可以被改進”呢?任何大家看得到的技術(shù)進步,不都是來自于對于已有技術(shù)的不滿嗎?如果人人都只知道提高自己的武功,卻不去改進武器,那么大家現(xiàn)在為什么不是還在用“機器碼”編程呢?
我明天會簡單分析一下“關(guān)系型數(shù)據(jù)庫對于OO的啟示”,后天就打算正式開始介紹自己的新的語言設計方案。希望大家沒有等得太生氣
敲響OO時代的喪鐘——OO能從關(guān)系型數(shù)據(jù)庫借鑒些什么?
今天這篇是關(guān)于OO VS. RDB的,OO作為一種編程范型,主要是集中于處理“操作”,而RDBMS作為一種數(shù)據(jù)管理工具,主要是集中于“數(shù)據(jù)”。但是,在一個需要數(shù)據(jù)庫的系統(tǒng)中,必然的情況是:操作的對象自然是各種各樣的數(shù)據(jù),而數(shù)據(jù)的管理,自然要通過操作。因此,OO與RDB,從最初淺的角度來理解,雖然分別位于“業(yè)務邏輯層”與“數(shù)據(jù)層”,但是相互之間卻又有著非常緊密的聯(lián)系。在OO與RDB之間存在著的緊張關(guān)系,其根源在于:“OO世界中的數(shù)據(jù)是被封裝的孤立數(shù)據(jù);RDB世界中的操作是針對行集的。”
因此,一個對象實例內(nèi)部的數(shù)據(jù),應該保存到數(shù)據(jù)庫中的哪些表、哪些行、哪些列,是一個非常復雜的問題。反過來說,當我們要在內(nèi)存中恢復一個對象實例時,要從多少表、多少行、多少列中采集數(shù)據(jù),并正確轉(zhuǎn)化為對象實例的內(nèi)部數(shù)據(jù),也是相當?shù)膹碗s。O/R Mapping,需要考慮的問題還不止于此,在RDB中自然存在的“關(guān)系”這一概念,在OO當中,卻沒有明確的對應概念。在OO的世界里,對象之間存在各種各樣的關(guān)系,卻非常難以進行規(guī)范化的描述。再加上:“添加、修改、刪除、查詢”等等情況的O/R映射,以及與“關(guān)系”相關(guān)的“級聯(lián)操作”——“級聯(lián)添加、級聯(lián)修改、級聯(lián)刪除、級聯(lián)查詢”,一個好的O/R Mapping工具,要做出來真是千難萬難。
很多人都意識到了這一點!是的,問題的確存在。但是,為什么呢?該怎么辦呢?幾乎沒有人會反思OO的不是,而是想當然的認為:“關(guān)系數(shù)據(jù)庫技術(shù)太老了,跟不上OO技術(shù)的進步,因此,我們需要OODB這樣的能夠直接支持OO的數(shù)據(jù)庫。”
“以其昏昏,使人昭昭”的事情,從來沒有發(fā)生過。依著我前面的分析,在OO這樣的基礎薄弱的理論上,怎么可能搞出有實用價值的數(shù)據(jù)庫呢?
在看到了徐昊的《關(guān)于面向?qū)ο笤O計與數(shù)據(jù)模型的精彩論述》之后,我相信自己找到了知音。他說:“OO在數(shù)據(jù)模型設計上不具有思維簡潔性。”并且提出了一個重要的詞匯:“邊語義”!這使我相信,和我有類似想法的同志,是存在的。后來又現(xiàn)場聽到了曹曉鋼同志的《ORM時代的數(shù)據(jù)訪問技術(shù)》的演講,并且在他的筆記本里看到了他做的一些代碼,居然與我的很多想法不謀而合!再加上后來與徐昊的幾次MSN交流,終于使我敢于開始寫這樣一篇“OO喪鐘”的文章,因為,我相信自己并不是孤獨的反對者。
OO可以從關(guān)系型數(shù)據(jù)庫那里借鑒些什么呢?
1、關(guān)系:也就是徐昊所說的邊語義。在 OO中,對象與對象之間是否存在關(guān)系,在對象之外是不知道的。當一個對象被封裝起來以后,他內(nèi)部是否使用、關(guān)聯(lián)、組合了其他的對象,是不可知的。因此,我們看到的通常的OO圖,只能說是Object被剖開了以后的對象圖。事實上,關(guān)系是被隱藏起來的。而在RDB中,關(guān)系非常明確的被定義與標識出來,一目了然。這將帶來巨大的描述效果。相比起UML Class圖,E-R要容易理解得多。
2、Primary Key:這是RDB 特有的概念,在OO中沒有對應概念。因此,我們要判斷兩個對象是否相等,就相當困難。如果每個對象都有一個“一次設置,終身不變的Primary Key”,那么對象之間的比較語義,就能夠被清楚的區(qū)分為:IS和LIKE。IS就是Primary Key相同的兩個對象,他們應該完全一致,甚至在內(nèi)存中,也只應該保存一份。LIKE,就是成員數(shù)據(jù)相同的兩個對象,他們不是一個東西,僅僅是像而已。
3、SQL:這也是RDB特有的語言,而在OO的世界里,查找一個對象的工作,從來沒有被規(guī)范過。
目前就先想到這些,可能還需要再補充。