繼承是代碼復用的最佳方案嗎?
繼承,一個父類可有許多個子類。父類就是把一些公共代碼放進去,之后在實現其他子類時,少寫一些代碼。
代碼復用,很多人覺得繼承就是絕佳方案。若把繼承理解成代碼復用,更多是站在子類角度向上看。在客戶端代碼使用時,面對的是子類,這種繼承叫實現繼承:
還有一種看待繼承的角度:從父類往下看,客戶端使用時,面對的是父類,這種繼承叫接口繼承:
但接口繼承更多和多態相關。本文主要討論實現繼承。
不推薦實現繼承:
? 繼承很寶貴,Java只支持單繼承 一個類只能有一個父類,一旦繼承的位置被實現繼承占據,再想做接口繼承就難了
? 實現繼承通常也是一種受程序設計語言局限的思維方式 很多語言,不使用繼承,也有代碼復用方案
1、案例
產品報表服務,其中的某服務:查詢產品信息。該查詢過程通用,別的服務也可用。所以,我把它放父類以復用:
ReportService沒有繼承任何類,但也可復用代碼,即ProductFetcher模塊。這樣,若我需要有個獲取產品信息的地方,它不必非得是個服務,我無需繼承任何類。
獲取產品信息、生成報表是兩件事,只是因為在生成報表過程,需要獲取產品信息,所以,它有個基類。
不用繼承的實現:
這就是組合:ReportService里組合一個ProductFetcher。設計通用原則:組合優于繼承。即若一個方案既能用組合實現,也能用繼承實現,那就用組合。
所以,要寫繼承以實現代碼復用時,問問自己,這是接口繼承,還是實現繼承?若是實現繼承,是不是可以寫成組合?
2、面向組合編程
可以組合的根因:獲取產品信息、生成報表服務本是兩件事(分離關注點)。你要是看出是兩件事了,就不會把它們放一起。
分解是設計的第一步,分解粒度越小越好。當可分解出多個關注點,每個關注點就是個獨立類。最終類由這一個個小類組合而得,即面向組合編程。按面向組合思維:為增加復雜度,增加一個報表生成器(ReportGenerator),在獲取產品信息后,生成報表:
OOP面向的是“對象”,不是類!很多程序員習慣把對象理解成類的附屬品,但在Alan Kay的理解中,對象本身就是獨立個體。所以,有些語言支持直接在對象操作。
現在,想給報表服務新增接口:處理產品信息。這樣的處理只會影響這里的一個對象,而同樣是這個ReportService的其他實例,則完全不受影響。
- ? 好處 不必寫那么多類,根據需要,在程序運行時組合出不同對象。
Java只有類這種組織方式,所以,很多有差異的概念只能用類這一個概念表示,思維受到限制,不同語言則提供不同的表現形式,讓概念更加清晰。
前面只是面向組合編程在思考方式的轉變,現在看設計差異。
3 案例
字體類(Font)需求:支持加粗、下劃線、斜體(Italic),且能任意組合。
3.1 繼承
需8個類:
3.2 組合
字體類(Font)只需三個獨立維度:是否加粗、下劃線、斜體。若再來一種需求,變成4種,采用繼承,類數量膨脹到16個,而組合只需再增加一個維度。把一個M*N問題,設計轉成M+N。
Java在面向組合編程方面能力較弱,但Java在嘗試不同方案。早期嘗試有Qi4j,后來Java 8加入default method,在一定程度上也可支持面向組合編程。
4、DCI
繼承是OOP原則之一,但編碼實踐中能用組合盡量使用組合。DCI也是一種編碼規范,對OOP的一種補充,核心思想也是關注點分離。
DCI是對象的Data數據, 對象使用的Context場景, 對象的Interaction交互行為三者簡稱, 是一種特別關注行為的模式(可對應GoF行為模式),而MVC模式是一種結構性模式,DCI可使用演員場景表演來解釋,某實體在某場景中扮演包公,實施包公升堂行為;典型事例是銀行帳戶轉帳,轉帳這行為按DDD很難劃分到帳號對象,它是跨兩個帳號實例之間的行為,可看成是帳號這個實體(PPT,見四色原型)在轉帳這個場景,實施了鈔票劃轉行為,這種新角度更貼近需求和自然,結合四色原型 DDD和DCI可以一步到位將需求更快地分解落實為可運行的代碼,是國際上軟件領域的一場革命。摘自 https://www.jdon.com/dci.html
5、總結
組合優于繼承。 復用方式背后的編程思想:面向組合編程。它給我們提供了一個不同的視角,但支撐面向組合編程的是分離關注點。將不同關注點分離,每個關注點成為一個模塊,在需要時組裝。面向組合編程,在設計本身上有很多優秀地方,可降低程序復雜度,更是思維轉變。
參考
? https://www.infoq.cn/article/2007/11/qi4j-intro
? https://en.wikipedia.org/wiki/Data,_context_and_interaction