組合大于繼承(Composition over Inheritance)肠牲?這是一個問題幼衰。
繼承是一個縱向的擴展,組合是一個橫向的擴展缀雳。
繼承是從代碼復用的角度來說的渡嚣。但是繼承并不直接等同于代碼復用,如果一開始只是出于代碼復用的目的而不區(qū)分類別和場景,就采用繼承是不恰當?shù)摹?/p>
代碼復用還可以使用組合的方式识椰,比如前端的React框架有著一套極為強大的組合機制绝葡,官方也強烈建議使用組合的方式來應用React。
就大部分開發(fā)任務來說腹鹉,其實繼承出現(xiàn)的場景不多藏畅,主要還是代碼復用的場景比較多,然而如果通過組合去進行代碼復用又會顯得比較麻煩功咒。
從維護成本來看愉阎,組合其實會更好——因為不恰當?shù)厥褂美^承會導致高耦合:從而破壞封裝,子類依賴于父類的實現(xiàn)力奋,子類缺乏獨立性榜旦。如果有人不小心改動了父類,就會牽一發(fā)而動全身刊侯,所有涉及到的子類的功能都有可能產(chǎn)生問題章办。
同時繼承還會帶來并不需要的屬性或者行為方法。怎么辦滨彻?要么覆寫藕届、要么換一個父類繼承(這個方式貌似會導致出現(xiàn)其他的并不需要的屬性或者行為方法ORZ...)。
覆寫同樣會導致問題亭饵,比如A是B的父類休偶,他有一個屬性嘴巴,這個嘴巴有一個說話和吃飯的功能辜羊,B繼承了它踏兜,但是B不需要會說話,它吃飯就行了——于是它覆寫了——這將可能導致其他繼承自A的子類都變成啞巴八秃。
一般如果硬要使用繼承來使用的話碱妆,更通用的方法是,寫一個基類昔驱,重寫方法疹尾,然后更改一堆繼承關(guān)系,還是很麻煩啊骤肛。
所以繼承得遵循LSP(里氏替換原則):子類可以擴展父類的功能纳本,但不能改變父類原有的功能。
它包含以下4層含義:
1. 子類可以實現(xiàn)父類的抽象方法腋颠,但不能覆蓋父類的非抽象方法繁成。
2. 子類中可以增加自己特有的方法。
3. 當子類的方法重載父類的方法時淑玫,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松巾腕。
4. 當子類的方法實現(xiàn)父類的抽象方法時面睛,方法的后置條件(即方法的返回值)要比父類更嚴格。
提到組合就出現(xiàn)了另一個原則——CARP(合成/聚合復用原則):
合成/聚合復用原則(Composite/Aggregate Reuse Principle或CARP)經(jīng)常又叫做合成復用原(Composite Reuse Principle或CRP)祠墅,就是在一個新的對象里面使用一些已有的對象侮穿,使之成為新對象的一部分;新對象通過向這些對象的委派達到復用已有功能的目的毁嗦。
React框架強烈建議組合亲茅,并以函數(shù)式編程單一功能原則——一個函數(shù)或者說組件只實現(xiàn)一個功能。那么這里也就出現(xiàn)了——使用組合的方式可能會比使用繼承寫更多的代碼狗准。
因為組合的問題是:
1. 整體類不能自動獲得和局部類同樣的接口克锣。
2. 創(chuàng)建整體類的對象時,需要創(chuàng)建所有局部類的對象(或者說組合所有的局部類)腔长。
綜上袭祟,使用組合和繼承應該遵循以下幾個條件:
1. 精心設(shè)計專門用于被繼承的類,繼承樹的抽象層應該比較穩(wěn)定捞附,一般不要多于三層巾乳;
2. 對于不是專門用于被繼承的類,禁止其被繼承鸟召;
3. 優(yōu)先考慮用組合關(guān)系來提高代碼的可重用性胆绊;
4. 子類是一種特殊的類型,而不只是父類的一個角色欧募;
5. 子類擴展压状,而不是覆蓋或者使父類的功能失效。
或許用面向接口編程的設(shè)計思想能比較好的解決繼承導致的高耦合的問題(注意:面向接口編程和面向?qū)ο缶幊滩⒉皇瞧郊壍母蹋⒉皇潜让嫦驅(qū)ο缶幊谈冗M的一種獨立的編程思想种冬,而是附屬于面向?qū)ο笏枷塍w系,屬于其一部分舔糖∮榱剑或者說,它是面向?qū)ο缶幊腆w系中的思想精髓之一金吗。)