“我是舊時代的殘黨,新時代沒有承載我的船,”
如果面向物件編程是一個人,我猜他自己在不斷被非議的今天,一定會這樣感慨,
說實話,我用面向物件方式編程已經十幾年了,我做架構設計離不開它,做系統分析離不開它,編碼的時候更是嚴重依賴它,我對面向物件無論是思想上還是寫代碼上都對它是有很深的感情,
剛學 Java 的時候,我覺得面向物件編程(OOP)真牛逼,用面向物件方式寫出來的代碼是最好的代碼,但是隨著專案越做越多,代碼越寫越多,我發現 OOP 不是萬能的,盲目的迷信追求 OOP 會有代價,
今天這篇文章我不是說面向物件不好,只是希望大家不要過度神話它,更不要人云亦云,
大家都聽說過
面向物件的三大特性:繼承、封裝、多型
但其實這個說法有問題,面向物件的思想里沒有任何繼承和多型的概念,正確的說法是:
這三大特性是面向物件語言的特性,而不是面向物件理念本身的,
面向物件語言是面向物件設計思想的一種實作,面向物件語言為了能在真實世界使用,其必須經過一些拓展和妥協,而問題也就隨著這些拓展和妥協而來,
1. 繼承帶來的也可能是無以復加的痛苦
在實際開發中,我們無論誰寫代碼,都要考慮代碼的復用性,面向物件的編程語言作為給開發人員使用的工具,它也必須考慮到復用性,
所以,在面向物件編程語言里,對面向物件的基礎思想做了拓展,搞出了繼承這個概念,
繼承就具體實作來說,就是子類擁有父類的所有非 private 的屬性和方法,繼承的出現能夠最大化的代碼復用,
當專案里一個類已經有了我們需要的屬性和方法,而我們現在的需求只是在這個已有類的基礎上有些許的不同,我們只需要繼承這個類,僅把這少許的不同在子類中實作即可,
但是如果你用了繼承,你就引入了問題,
繼承的出現天然會使得子類和父類緊耦合,也就是說,父類和子類是緊密關聯的,牽一發動全身,
如果現實世界里,所有業務模型都是有層次的,而且層次井然有序,是一顆天然的樹,那這種緊耦合沒有什么問題,
但是現實的需求可不是吃干飯的!
咱們看看這樣一種情況,假設現在我們一家只有兩口人,即只有父親和孩子,那么類繼承模型很容易模擬這種情況:

我們在現實生活里,往往是三口之家:

那這就有問題了,就像小時候經常有人會問孩子,你覺得你是爸爸的孩子,還是媽媽的孩子啊?如果你要用 Java 的規矩回答,只能從是爸爸或者媽媽里選一個,那么完蛋了,回答爸爸的孩子,媽媽不高興;回答媽媽的孩子,問題更嚴重,隔壁老王?
但是,如果像 C++ 那樣,你說我既是爸爸的孩子也是媽媽的孩子,也有問題,
假設爸爸類里有個方法叫說話,媽媽類也有個方法叫說話,你作為繼承了他們的孩子類,自然也會擁有說話這個方法,問題來了,你所擁有的的說話這個方法到底來源于誰?
另外咱們說了,繼承會把子類和父類緊耦合,一旦業務模型失配,就會造成問題,
這里給出一個維基百科舉的經典例子,來說明一下:
class Super {
private int counter = 0;
void inc1() {
counter++;
}
void inc2() {
counter++;
}
}
class Sub extends Super {
@Override
void inc2() {
inc1();
}
}
你看,子類覆寫了父類的 inc2 方法,但是這個 inc2 方法依賴于父類 inc1 的實作,
如果父類的 inc1 邏輯發生變化了,變成下面這樣
class Super {
private int counter = 0;
void inc1() {
inc2();
}
void inc2() {
counter++;
}
}
這就會出現 stack overflow 的例外,因為出現了無限遞回,
所以,當我們在子類里,依賴了父類方法作為子類業務邏輯的一個關鍵步驟的時候,當父類的邏輯修改的時候,必須聯動修改所有依賴父類相關邏輯的子類,否則就可能引發嚴重的問題,
用繼承,本來是想少寫點代碼少加點班,結果……用網上看到的一句話說就是:
一日為父,終生是祖宗,
像這種情況該怎么辦?
現在只要是個正經的介紹面向物件的技術文章或者書籍里,只要是涉及到繼承的,都會加這么句話:
盡量選擇物件組合的設計方式,
在《阿里巴巴Java開發手冊》中就有一條:

組合和繼承的區別如下:

其實我認為繼承和組合各有優缺點,如果兩個類確實非常緊密,就是存在層次關系,用繼承沒問題,
之所以有“組合優于繼承”這個說法,我個人感覺是組合更靈活,而且能防止被人濫用,用不好的話輕則類的層次失控,重則很可能就把整個專案的代碼質量給腐蝕了,
2. 封裝如同帶有漏洞的封印,可能會逃逸出魔王
封裝,說白了就是把屬性、方法,封到一個物件里,這是面向物件的核心理念,
嘴上叫封裝,卻開了個縫兒,
我們知道,專案是既要兼顧代碼質量,還要兼顧運行性能的,不可能說為了提升什么松耦合、高內聚,就不管不顧性能了,
事情就壞在了這個兼顧性能這里,面向物件里,以上帝角度看,系統就是物件和物件之間的關系構造成的網路,
就拿咱們上面談到的組合關系來說,組合關系的實作就是通過把一個物件當成另一個物件的屬性來實作的,

上面這圖就叫做 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. 多型好,但可能是面向物件的貪天之功
再說說多型,
其實,面向物件中的多型使用,才是面向物件語言最被認可的地方,因為有了多型,代碼才能保證在業務需求多變的情況下,保證了專案的相對穩定,
可是,多型不是面向物件獨有的啊,面向程序,函式式編程也可以:面向程序里,C 語言可以靠虛函式去在運行時加載對應的函式實作去實作多型,函式式編程也可以通過組合函式去實作多型,
所以,面向物件連多型這種優勢都不獨特了,
4. 服務端業務變了,人們的觀點發生變化了
在說服務端業務的變化之前,我想先普及兩個概念,即有狀態的服務和無狀態的服務,
有狀態的服務就是說,服務需要暫時存一些和客戶端相關的資料,以便客戶端后續發來的請求可以和客戶端前面發的請求通過服務器端關聯起來,從而共同完成一項業務,
無狀態服務是說,服務端不存盤任何和客戶端相關的資料,客戶端每次請求,服務端都認為這是個新客戶端,和以前的請求無任何關系,
用現實生活舉例的話,有狀態服務就是你去一家健身房,第一次去的時候花了一筆錢辦了一張健身卡,你以后每次去健身,有卡就不用再掏錢了,
無狀態服務就是,你沒辦卡,每次去都和第一次去一樣現掏錢,
那么,無狀態服務和有狀態服務和面向物件的衰落又有什么關系呢?在如今的年代,分布式、微服務大行其道,一個有狀態的服務是不容易做分布式和做彈性伸縮的,
當年,大家做有多個步驟的業務的時候,為了保證業務資料不會因為用戶偶然的關閉瀏覽器或者瀏覽器崩潰等問題而丟失,往往會把上一個步驟的資訊存在服務端的 session 里,而現在則會傾向考慮把資訊放在客戶端的本地存盤上,
我舉個例子,假設現在有個需求,要在后臺系統新增加一個功能:用戶資訊管理,其中有個需求要求這樣操作,錄入用戶資訊分成兩步,
-
第一步,錄入用戶的基本資訊:姓名、手機號、年齡……
-
第二步,錄入額外資訊:家庭成員、教育經歷、作業經歷……
出于資訊完整度的考慮,業務要求這兩步應該是一個完整的事務,要么都成功,要么都失敗,
從技術實作上講,如果是多年以前,我們會在第一步的時候,把商戶的基本資訊做成表單提交,然后為了保證不會因為用戶誤關閉瀏覽器等意外問題丟失中間的資料,保存在對應的 session 中后,在第二步資訊提交后,合并起來一起存入到資料庫中,
但是,現在的技術趨勢是,做任何事情,盡量讓服務器端無狀態,也就是不存盤客戶端相關資料,
此時,這個需求的解決方案就是,當第一步填寫商戶資訊完成后,直接把資料存盤在客戶端的本地存盤里又或者直接就存在 cookie 里,在第二步填寫內容完畢后,聯合存在客戶端的資訊一起提交到服務器端,然后存入資料庫,
所以,你看到了,現在大家的趨勢就是服務器端都在轉向無狀態服務,哪怕以前是有狀態的服務,也會通過一些增加客戶端引數等手段,去改造為無狀態服務,
說了這么多,那這種技術趨勢的變化對我們的面向物件有什么影響呢?
影響在于,服務端現在越來越變得往單純的處理資料這個方向發展,當僅處理資料的時候,服務器端真正的需求其實就是計算,然后就是為了大幅度提升計算速度,而帶來的并行化需求,
而面向物件這種方式和我們當今的技術趨勢是有一些沖突的,
首先就是確定性的沖突,
我們的首要需求從以前重度處理業務狀態加業務資料變成了業務資料的計算,而計算是需要確定性的:即給定相同的輸入,經過服務器端相同的邏輯處理后,應該給定相同的輸出,
而面向物件這種方式,出身在有狀態服務大行其道的年代,它會優先考慮業務邏輯的調度,其次才是計算,所以,面向物件是擁有狀態的,面向物件的狀態就是它的欄位值,這些欄位值,如果單純的從計算資料角度看,他們不僅無意義了,反而還引入了風險,
比如,我們不小心把一個物件的狀態給共享出去了,那當我們用同樣的輸入計算的時候,很可能由于狀態的變化,導致了不同的輸出結果,最后就是專案出了問題,
其次,由于計算我們對性能更加看重了,又由于無狀態服務的大量使用,所以,并行的重要性也遠遠超出了以前,而并行,要求的是結構的開放,和更加嚴格的無狀態化,而面向物件,恰恰嚴重依賴于狀態,并且,他還把這種狀態依賴封裝在了復雜的物件關系里,
A 狀態依賴于 B 的狀態,B 的狀態又依賴于 C,而這些依賴,全部被封裝在了 D 物件的實作細節里,這種嚴重的反并行也是現在越來越多人開始反感面向物件的重要原因,
結尾
說了這么多面向物件的壞話,其實真的是面向物件自身的問題嗎?并不是,
首先,面向物件其實就是我們程式員試圖簡化這個世界,提高對這個世界的認知的一種美好愿望而已,愿望來自于人自身認知的局限性,所以本身就不可能完美,
其次,面向物件編程語言只是一種工具,工具的使用的好壞還是要靠人的,不可能每個人能把一套工具用的完美無缺,
如上所說,面向物件的問題本質還是人的問題,而人可能永遠都需要通過組合使用越來越多的類似面向物件的這種并不完美的工具去解決自己的問題,
所以,我們不能一味的依靠面向物件,認為面向物件就是最棒的,也不能發現面向物件可能應付不了某些業務場景了,就開始極端地摒棄它,
我們要靈活地,合理地使用任何我們可以使用的編程思想、編程工具,積極地去擁抱變化,
不要忘了我們寫代碼的初衷,
你好,我是四猿外,
一家上市公司的技術總監,管理的技術團隊一百余人,
我從一名非計算機專業的畢業生,轉行到程式員,一路打拼,一路成長,
我會把自己的成長故事寫成文章,把枯燥的技術文章寫成故事,
歡迎關注我的公眾號,關注后可以領取高并發、演算法學習資料,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/417114.html
標籤:其他
