面向對象編程之丑?
[導讀]“我是舊時代的殘黨,新時代沒有承載我的船?!比绻嫦驅ο缶幊淌且粋€人,我猜他自己在不斷被非議的今天,一定會這樣感慨。說實話,我用面向對象方式編程已經十幾年了,我做架構設計離不開它,做系統(tǒng)分析離不開它,編碼的時候更是嚴重依賴它,我對面向對象無論是思想上還是寫代碼上都對它是有很深的感...
“我是舊時代的殘黨,新時代沒有承載我的船?!?/p>如果面向對象編程是一個人,我猜他自己在不斷被非議的今天,一定會這樣感慨。說實話,我用面向對象方式編程已經十幾年了,我做架構設計離不開它,做系統(tǒng)分析離不開它,編碼的時候更是嚴重依賴它,我對面向對象無論是思想上還是寫代碼上都對它是有很深的感情。剛學 Java 的時候,我覺得面向對象編程(OOP)真牛逼,用面向對象方式寫出來的代碼是最好的代碼。但是隨著項目越做越多,代碼越寫越多,我發(fā)現(xiàn) OOP 不是萬能的,盲目的迷信追求 OOP 會有代價。今天這篇文章我不是說面向對象不好,只是希望大家不要過度神話它,更不要人云亦云。大家都聽說過
我們在現(xiàn)實生活里,往往是三口之家:
那這就有問題了。就像小時候經常有人會問孩子,你覺得你是爸爸的孩子,還是媽媽的孩子???如果你要用 Java 的規(guī)矩回答,只能從是爸爸或者媽媽里選一個,那么完蛋了?;卮鸢职值暮⒆?,媽媽不高興;回答媽媽的孩子,問題更嚴重,隔壁老王?但是,如果像 C 那樣,你說我既是爸爸的孩子也是媽媽的孩子,也有問題。假設爸爸類里有個方法叫說話,媽媽類也有個方法叫說話,你作為繼承了他們的孩子類,自然也會擁有說話這個方法。問題來了,你所擁有的的說話這個方法到底來源于誰?另外咱們說了,繼承會把子類和父類緊耦合,一旦業(yè)務模型失配,就會造成問題。這里給出一個維基百科舉的經典例子,來說明一下:
組合和繼承的區(qū)別如下:
其實我認為繼承和組合各有優(yōu)缺點,如果兩個類確實非常緊密,就是存在層次關系,用繼承沒問題。之所以有“組合優(yōu)于繼承”這個說法,我個人感覺是組合更靈活,而且能防止被人濫用,用不好的話輕則類的層次失控,重則很可能就把整個項目的代碼質量給腐蝕了。
上面這圖就叫做 A 和 B 之間是組合關系。想用 A 對象里的 B 對象,代碼這么寫:
面向對象的三大特性:繼承、封裝、多態(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)在我們一家只有兩口人,即只有父親和孩子,那么類繼承模型很容易模擬這種情況:

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ā)手冊》中就有一條:


2. 封裝如同帶有漏洞的封印,可能會逃逸出魔王
封裝,說白了就是把屬性、方法,封到一個對象里,這是面向對象的核心理念。嘴上叫封裝,卻開了個縫兒。我們知道,項目是既要兼顧代碼質量,還要兼顧運行性能的。不可能說為了提升什么松耦合、高內聚,就不管不顧性能了。事情就壞在了這個兼顧性能這里。面向對象里,以上帝角度看,系統(tǒng)就是對象和對象之間的關系構造成的網絡。就拿咱們上面談到的組合關系來說,組合關系的實現(xiàn)就是通過把一個對象當成另一個對象的屬性來實現(xiàn)的。
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)新增加一個功能:用戶信息管理。其中有個需求要求這樣操作,錄入用戶信息分成兩步。- 第一步,錄入用戶的基本信息:姓名、手機號、年齡……
- 第二步,錄入額外信息:家庭成員、教育經歷、工作經歷……





