為什么“類(lèi)繼承”是有害的?

很多Java程序員喜歡將“面向?qū)ο笕筇匦浴狈顬楣玺貏e是“繼承”,在他們眼中,“繼承”是“面向?qū)ο蟆弊钪匾奶匦裕聦?shí)上廣義的“面向?qū)ο蟆辈](méi)有限定必需“繼承”,甚至“面向?qū)ο蟆边@個(gè)概念在業(yè)界上也還沒(méi)達(dá)成統(tǒng)一,比如像Go和Rust這類(lèi)不支持“繼承”的編程語(yǔ)言,也有很多人認(rèn)為它們是“面向?qū)ο蟆钡恼Z(yǔ)言。

今天我就想給很多Java程序員們潑一盤(pán)冷水:“類(lèi)繼承”是有害的。

為什么我明確是“類(lèi)繼承”呢?因?yàn)椤敖涌诶^承”并不在此列,所以需要特別指出。

我的觀點(diǎn)是:在所有的大型Java項(xiàng)目中,凡是最難以維護(hù)、難以修改、難以閱讀、牽一發(fā)而動(dòng)全身的代碼,必然是那些使用了“類(lèi)繼承”的代碼。

這是因?yàn)椤邦?lèi)繼承”違反了一個(gè)設(shè)計(jì)原則:最小職責(zé)原則。

對(duì)于父類(lèi)來(lái)說(shuō),它即是實(shí)現(xiàn)又是抽象。實(shí)現(xiàn)是對(duì)于父類(lèi)本身而言,它可以被實(shí)例化成對(duì)象;而抽象則是對(duì)于子類(lèi)來(lái)說(shuō),它是子類(lèi)的抽象。這兩個(gè)特性分開(kāi)來(lái)都說(shuō)好特性,組合在一起就變成了難吃的五仁月餅。

對(duì)于實(shí)現(xiàn)而言,父類(lèi)就不可能做得太小,因?yàn)樗怯泄δ苄缘?,即使它把單一的功能拆散成多個(gè)類(lèi)再組合起來(lái),這樣對(duì)于子類(lèi)來(lái)說(shuō),也是繼承了父類(lèi)的全部,而這恰恰造成了很大的耦合度——要是在將來(lái)重構(gòu)發(fā)現(xiàn)子類(lèi)不需要某個(gè)方法,也無(wú)法去掉,因?yàn)橐坏┤サ?,就不滿足父類(lèi)了。你可能覺(jué)得可以在方法體里拋個(gè)異常即可,但是對(duì)于調(diào)用方來(lái)說(shuō),一旦不知道這一點(diǎn),就會(huì)出現(xiàn)BUG,一旦系統(tǒng)中這種設(shè)計(jì)越來(lái)越多,整個(gè)系統(tǒng)就會(huì)混亂不堪,就像縫縫補(bǔ)補(bǔ)的破褲子一樣。

而對(duì)于抽象來(lái)說(shuō),父類(lèi)承擔(dān)起了接口的責(zé)任,一個(gè)巨大的接口,這也是Java程序員津津樂(lè)道的“多態(tài)”。但是可怕的是,父類(lèi)的多態(tài)類(lèi),有可能是它繼承樹(shù)下面的所有子類(lèi)中的其中一個(gè),而這個(gè)子類(lèi),又可能是其他子類(lèi)的父類(lèi),你需要了解整個(gè)繼承鏈路上的所有子類(lèi);而對(duì)于接口而言,實(shí)現(xiàn)類(lèi)就是一張鋪平的網(wǎng),你要了解的這里的其中一個(gè)即可。

經(jīng)驗(yàn)豐富的Java程序員有一種說(shuō)法:“類(lèi)繼承不能夠超過(guò)三層,否則就會(huì)造成混亂”。這是根據(jù)經(jīng)驗(yàn)而得出的結(jié)論,而對(duì)于“接口繼承”來(lái)說(shuō),卻沒(méi)有這種說(shuō)法,一方面是接口很少情況會(huì)需要這么多層的繼承,另一方面是即使超過(guò)100層的接口繼承,也不會(huì)造成混亂。這是為什么?

因?yàn)椤邦?lèi)繼承”的混亂是指數(shù)級(jí)別的增長(zhǎng),而“接口繼承”則是線性增長(zhǎng)。對(duì)于混亂程度,物理學(xué)上把這種程度定義成熵,熵的符號(hào)是S,假設(shè)繼承層數(shù)是N,那么,“類(lèi)繼承”的熵就是O^N,而“接口繼承”則是N*S

在多層繼承里,方法的覆蓋會(huì)非常混亂,你不知道方法到底在哪里被覆蓋了,繼承鏈路上所有子類(lèi)都有可能是罪魁禍?zhǔn)?。而?duì)于接口來(lái)說(shuō),即使是接口中的方法是默認(rèn)方法,也沒(méi)關(guān)系,因?yàn)橹挥袑?shí)現(xiàn)類(lèi)最終會(huì)調(diào)用到方法,更重要的是,接口不會(huì)被實(shí)例化,因此它可以包含最少的方法,就是所謂的單一職責(zé)原則,而讓實(shí)現(xiàn)類(lèi)實(shí)現(xiàn)多個(gè)接口。這樣接口就沒(méi)有必要去繼承了。

隨著組合由于繼承的觀念深入人心,很多主流的框架和組件都放棄了繼承,比如Spring,你基本上不需要用到繼承,除非你自作主張。

在Java8之前,“類(lèi)繼承”尚且有他存在的意義,那就是代碼復(fù)用;而在Java8之后,由于接口有了默認(rèn)方法這一特性,“類(lèi)繼承”就變成了徹頭徹尾的垃圾——抱歉我用“垃圾”兩字來(lái)形容它,因?yàn)槲艺娴牟幌肟吹侥切┦褂昧死^承還沾沾自喜的代碼了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容