“我是舊時代的殘黨,新時代沒有承載我的船。”如果
面向對象編程 是一個人,我猜他自己在不斷被非議的今天,一定會這樣感慨。說實話,我用面向對象方式編程已經十幾年了,我做架構設計離不開它,做系統(tǒng)分析離不開它,編碼的時候更是嚴重依賴它,我對面向對象無論是思想上還是寫代碼上都對它是有很深的感情。剛學 Java 的時候,我覺得面向對象編程(OOP)真牛逼,用面向對象方式寫出來的代碼是最好的代碼。但是隨著項目越做越多,代碼越寫越多,我發(fā)現(xiàn) OOP 不是萬能的,盲目的迷信追求 OOP 會有代價。今天這篇文章我不是說面向對象不好,只是希望大家不要過度神話它,更不要人云亦云。大家都聽說過
面向對象的三大特性:繼承、封裝、多態(tài) 但其實這個說法有問題。面向對象的思想里沒有任何繼承和多態(tài)的概念,正確的說法是:這三大特性是面向對象語言的特性,而不是面向對象理念本身的。面向對象語言是面向對象設計思想的一種實現(xiàn),面向對象語言為了能在真實世界使用,其必須經過一些拓展和妥協(xié),而問題也就隨著這些拓展和妥協(xié)而來。
1. 繼承帶來的也可能是無以復加的痛苦 在實際開發(fā)中,我們無論誰寫代碼,都要考慮代碼的復用性。面向對象的編程語言作為給開發(fā)人員使用的工具,它也必須考慮到復用性。所以,在面向對象編程語言里,對面向對象的基礎思想做了拓展,搞出了繼承這個概念。繼承就具體實現(xiàn)來說,就是子類擁有父類的所有非 private 的屬性和方法。繼承的出現(xiàn)能夠最大化的代碼復用。當項目里一個類已經有了我們需要的屬性和方法,而我們現(xiàn)在的需求只是在這個已有類的基礎上有些許的不同,我們只需要繼承這個類,僅把這少許的不同在子類中實現(xiàn)即可。但是如果你用了繼承,你就引入了問題。繼承的出現(xiàn)天然會使得子類和父類緊耦合。也就是說,父類和子類是緊密關聯(lián)的,牽一發(fā)動全身。如果現(xiàn)實世界里,所有業(yè)務模型都是有層次的,而且層次井然有序,是一顆天然的樹,那這種緊耦合沒有什么問題。但是現(xiàn)實的需求可不是吃干飯的!咱們看看這樣一種情況。假設現(xiàn)在我們一家只有兩口人,即只有父親和孩子,那么類繼承模型很容易模擬這種情況:
我們在現(xiàn)實生活里,往往是三口之家:
那這就有問題了。就像小時候經常有人會問孩子,你覺得你是爸爸的孩子,還是媽媽的孩子啊?如果你要用 Java 的規(guī)矩回答,只能從是爸爸或者媽媽里選一個,那么完蛋了?;卮鸢职值暮⒆樱瑡寢尣桓吲d;回答媽媽的孩子,問題更嚴重,隔壁老王?但是,如果像 C 那樣,你說我既是爸爸的孩子也是媽媽的孩子,也有問題。假設爸爸類里有個方法叫說話,媽媽類也有個方法叫說話,你作為繼承了他們的孩子類,自然也會擁有說話這個方法。問題來了,你所擁有的的說話這個方法到底來源于誰?另外咱們說了,繼承會把子類和父類緊耦合,一旦業(yè)務模型失配,就會造成問題。這里給出一個維基百科舉的經典例子,來說明一下:
class ?Super ? { ??private ?int ?counter?=?0 ; ??void ?inc1 () ? { ????counter ; ??} ??void ?inc2 () ? { ????counter ; ??} }class ?Sub ?extends ?Super ? { ??@Override ??void ?inc2 () ? { ????inc1(); ??} }
你看,子類覆蓋了父類的 inc2 方法,但是這個 inc2 方法依賴于父類 inc1 的實現(xiàn)。如果父類的 inc1 邏輯發(fā)生變化了,變成下面這樣
class ?Super ? { ??private ?int ?counter?=?0 ; ??void ?inc1 () ? { ????inc2(); ??} ??void ?inc2 () ? { ????counter ; ??} }
這就會出現(xiàn) stack overflow 的異常,因為出現(xiàn)了無限遞歸。所以,當我們在子類里,依賴了父類方法作為子類業(yè)務邏輯的一個關鍵步驟的時候,當父類的邏輯修改的時候,必須聯(lián)動修改所有依賴父類相關邏輯的子類,否則就可能引發(fā)嚴重的問題。用繼承,本來是想少寫點代碼少加點班,結果……用網上看到的一句話說就是:
一日為父,終生是祖宗。 像這種情況該怎么辦?現(xiàn)在只要是個正經的介紹面向對象的技術文章或者書籍里,只要是涉及到繼承的,都會加這么句話:盡量選擇對象組合的設計方式。在《阿里巴巴Java開發(fā)手冊》中就有一條:
組合和繼承的區(qū)別如下:
其實我認為繼承和組合各有優(yōu)缺點,如果兩個類確實非常緊密,就是存在層次關系,用繼承沒問題。之所以有“組合優(yōu)于繼承”這個說法,我個人感覺是組合更靈活,而且能防止被人濫用,用不好的話輕則類的層次失控,重則很可能就把整個項目的代碼質量給腐蝕了。
2. 封裝如同帶有漏洞的封印,可能會逃逸出魔王 封裝,說白了就是把屬性、方法,封到一個對象里,這是面向對象的核心理念。嘴上叫封裝,卻開了個縫兒。我們知道,項目是既要兼顧代碼質量,還要兼顧運行性能的。不可能說為了提升什么松耦合、高內聚,就不管不顧性能了。事情就壞在了這個兼顧性能這里。面向對象里,以上帝角度看,系統(tǒng)就是對象和對象之間的關系構造成的網絡。就拿咱們上面談到的組合關系來說,組合關系的實現(xiàn)就是通過把一個對象當成另一個對象的屬性來實現(xiàn)的。
上面這圖就叫做 A 和 B 之間是組合關系。想用 A 對象里的 B 對象,代碼這么寫:
A?a?=?new ?A(); B?b?=?a.getB();
好,我們要問了,這個從 A 中獲取的 B,是 B 對象的實例還是實例的一個引用指針呢?必然是引用指針吧,這是最基礎的知識。諾,問題來了,引用指針是可以修改的。
b.getS();?//原來是Hello?World b.setS("World" );//直接改成World
原來 B 中有個字段 s,值是個 “Hello World”,我直接可以用代碼改成“World”。如果這次修改隨意在個犄角旮旯里,A 能知道嗎?A 蒙在鼓里,還以為一切盡在把控當中呢。你看,封裝的縫兒出來了吧。說句實話,就這種鬼操作,是非常難以排查的。像這種封裝了,但是又沒封裝的問題,我只想說“封裝的挺好的,下次別封裝了”。
3. 多態(tài)好,但可能是面向對象的貪天之功 再說說多態(tài)。其實,面向對象中的多態(tài)使用,才是面向對象語言最被認可的地方。因為有了多態(tài),代碼才能保證在業(yè)務需求多變的情況下,保證了項目的相對穩(wěn)定。可是,多態(tài)不是面向對象獨有的啊。面向過程,函數(shù)式編程也可以:面向過程里,C 語言可以靠虛函數(shù)去在運行時加載對應的函數(shù)實現(xiàn)去實現(xiàn)多態(tài)。函數(shù)式編程也可以通過組合函數(shù)去實現(xiàn)多態(tài)。所以,面向對象連多態(tài)這種優(yōu)勢都不獨特了。
4. 服務端業(yè)務變了,人們的觀點發(fā)生變化了 在說服務端業(yè)務的變化之前,我想先普及兩個概念,即有狀態(tài)的服務和無狀態(tài)的服務。有狀態(tài)的服務就是說,服務需要暫時存一些和客戶端相關的數(shù)據(jù),以便客戶端后續(xù)發(fā)來的請求可以和客戶端前面發(fā)的請求通過服務器端關聯(lián)起來,從而共同完成一項業(yè)務。無狀態(tài)服務是說,服務端不存儲任何和客戶端相關的數(shù)據(jù),客戶端每次請求,服務端都認為這是個新客戶端,和以前的請求無任何關系。用現(xiàn)實生活舉例的話,有狀態(tài)服務就是你去一家健身房,第一次去的時候花了一筆錢辦了一張健身卡,你以后每次去健身,有卡就不用再掏錢了。無狀態(tài)服務就是,你沒辦卡,每次去都和第一次去一樣現(xiàn)掏錢。那么,無狀態(tài)服務和有狀態(tài)服務和面向對象的衰落又有什么關系呢?在如今的年代,分布式、微服務大行其道。一個有狀態(tài)的服務是不容易做分布式和做彈性伸縮的。當年,大家做有多個步驟的業(yè)務的時候,為了保證業(yè)務數(shù)據(jù)不會因為用戶偶然的關閉瀏覽器或者瀏覽器崩潰等問題而丟失,往往會把上一個步驟的信息存在服務端的 session 里,而現(xiàn)在則會傾向考慮把信息放在客戶端的本地存儲上。我舉個例子,假設現(xiàn)在有個需求,要在后臺系統(tǒng)新增加一個功能:用戶信息管理。其中有個需求要求這樣操作,錄入用戶信息分成兩步。
第一步,錄入用戶的基本信息:姓名、手機號、年齡…… 第二步,錄入額外信息:家庭成員、教育經歷、工作經歷…… 出于信息完整度的考慮,業(yè)務要求這兩步應該是一個完整的事務。要么都成功,要么都失敗。從技術實現(xiàn)上講,如果是多年以前,我們會在第一步的時候,把商戶的基本信息做成表單提交,然后為了保證不會因為用戶誤關閉瀏覽器等意外問題丟失中間的數(shù)據(jù),保存在對應的 session 中后,在第二步信息提交后,合并起來一起存入到數(shù)據(jù)庫中。但是,現(xiàn)在的技術趨勢是,做任何事情,盡量讓服務器端無狀態(tài),也就是不存儲客戶端相關數(shù)據(jù)。此時,這個需求的解決方案就是,當?shù)谝徊教顚懮虘粜畔⑼瓿珊?,直接把?shù)據(jù)存儲在客戶端的本地存儲里又或者直接就存在 cookie 里,在第二步填寫內容完畢后,聯(lián)合存在客戶端的信息一起提交到服務器端,然后存入數(shù)據(jù)庫。所以,你看到了,現(xiàn)在大家的趨勢就是服務器端都在轉向無狀態(tài)服務,哪怕以前是有狀態(tài)的服務,也會通過一些增加客戶端參數(shù)等手段,去改造為無狀態(tài)服務。說了這么多,那這種技術趨勢的變化對我們的面向對象有什么影響呢?影響在于,服務端現(xiàn)在越來越變得往單純的處理數(shù)據(jù)這個方向發(fā)展。當僅處理數(shù)據(jù)的時候,服務器端真正的需求其實就是計算,然后就是為了大幅度提升計算速度,而帶來的并行化需求。而面向對象這種方式和我們當今的技術趨勢是有一些沖突的。首先就是確定性的沖突。我們的首要需求從以前重度處理業(yè)務狀態(tài)加業(yè)務數(shù)據(jù)變成了業(yè)務數(shù)據(jù)的計算,而計算是需要確定性的:即給定相同的輸入,經過服務器端相同的邏輯處理后,應該給定相同的輸出。而面向對象這種方式,出身在有狀態(tài)服務大行其道的年代,它會優(yōu)先考慮業(yè)務邏輯的調度,其次才是計算,所以,面向對象是擁有狀態(tài)的。面向對象的狀態(tài)就是它的字段值。這些字段值,如果單純的從計算數(shù)據(jù)角度看,他們不僅無意義了,反而還引入了風險。比如,我們不小心把一個對象的狀態(tài)給共享出去了,那當我們用同樣的輸入計算的時候,很可能由于狀態(tài)的變化,導致了不同的輸出結果,最后就是項目出了問題。其次,由于計算我們對性能更加看重了,又由于無狀態(tài)服務的大量使用,所以,并行的重要性也遠遠超出了以前。而并行,要求的是結構的開放,和更加嚴格的無狀態(tài)化,而面向對象,恰恰嚴重依賴于狀態(tài),并且,他還把這種狀態(tài)依賴封裝在了復雜的對象關系里。A 狀態(tài)依賴于 B 的狀態(tài),B 的狀態(tài)又依賴于 C,而這些依賴,全部被封裝在了 D 對象的實現(xiàn)細節(jié)里,這種嚴重的反并行也是現(xiàn)在越來越多人開始反感面向對象的重要原因。
結尾 說了這么多面向對象的壞話,其實真的是面向對象自身的問題嗎?并不是。首先,面向對象其實就是我們程序員試圖簡化這個世界,提高對這個世界的認知的一種美好愿望而已。愿望來自于人自身認知的局限性,所以本身就不可能完美。其次,
面向對象編程 語言只是一種工具,工具的使用的好壞還是要靠人的,不可能每個人能把一套工具用的完美無缺。如上所說,面向對象的問題本質還是人的問題,而人可能永遠都需要通過組合使用越來越多的類似面向對象的這種并不完美的工具去解決自己的問題。所以,我們不能一味的依靠面向對象,認為面向對象就是最棒的,也不能發(fā)現(xiàn)面向對象可能應付不了某些業(yè)務場景了,就開始極端地摒棄它。我們要靈活地,合理地使用任何我們可以使用的編程思想、編程工具,積極地去擁抱變化。不要忘了我們寫代碼的初衷。