什么是設(shè)計(jì)模式
在GoF(Gang of Four)的書籍《Design Patterns - Elements of Reusable Object-Oriented Software(設(shè)計(jì)模式-可復(fù)用面向?qū)ο筌浖幕A(chǔ))》中是這樣定義設(shè)計(jì)模式的:Christopher Alexander說(shuō)過:“每一個(gè)模式描述了一個(gè)在我們周圍不斷重復(fù)發(fā)生的問題以及該問題的解決方案的核心硫惕。這樣裁着,你就能一次又一次地使用該方案而不必做重復(fù)勞動(dòng)” [AIS+77沽翔,第10頁(yè)]。盡管Alexander所指的是城市和建筑模式气破,但他的思想也同樣適用于于面向?qū)ο笤O(shè)計(jì)模式焕数,只是在面向?qū)ο蟮慕鉀Q方案里泽疆, 我們喲偶那個(gè)對(duì)象和接口代替了墻壁和門窗要门。兩類模式的核心都在于提供了相關(guān)問題的解決方案。一般而言冠蒋,設(shè)計(jì)模式有四個(gè)基本要素:
- 1羽圃、模式名稱(pattern name):一個(gè)助記名,它用一兩個(gè)詞來(lái)描述模式的問題抖剿、解決方案和效果朽寞。
- 2、問題(problem):描述了應(yīng)該在何時(shí)使用模式斩郎。
- 3脑融、解決方案(solution):描述了設(shè)計(jì)的組成成分,它們之間的相關(guān)關(guān)系以及各自的職責(zé)和協(xié)作方案缩宜。
- 4肘迎、效果(consequences):描述了模式應(yīng)用的效果以及使用模式應(yīng)該權(quán)衡的問題。
設(shè)計(jì)模式的創(chuàng)始人很明確地指出了設(shè)計(jì)模式的基本要素锻煌,但是由于現(xiàn)實(shí)中浮躁妓布、偏向過度設(shè)計(jì)等因素的干擾,開發(fā)者很多時(shí)候會(huì)重點(diǎn)關(guān)注第1和第3點(diǎn)要素(過度關(guān)注設(shè)計(jì)模式和設(shè)計(jì)模式的實(shí)現(xiàn))宋梧,忽略第2和第4點(diǎn)要素(忽視使用設(shè)計(jì)模式的場(chǎng)景和目標(biāo))匣沼,導(dǎo)致設(shè)計(jì)出來(lái)的編碼邏輯可能過于復(fù)雜或者達(dá)不到預(yù)期的效果。
總的來(lái)說(shuō)捂龄,設(shè)計(jì)模式(Design Pattern)是一套被反復(fù)使用释涛、多數(shù)人知曉的、經(jīng)過分類編目的倦沧、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)唇撬。也就是本來(lái)并不存在所謂設(shè)計(jì)模式,用的人多了刀脏,也便成了設(shè)計(jì)模式局荚。
設(shè)計(jì)模式的七大原則
面向?qū)ο蟮脑O(shè)計(jì)模式有七大基本原則:
- 開閉原則(Open Closed Principle,OCP)
- 單一職責(zé)原則(Single Responsibility Principle, SRP)
- 里氏代換原則(Liskov Substitution Principle愈污,LSP)
- 依賴倒轉(zhuǎn)原則(Dependency Inversion Principle,DIP)
- 接口隔離原則(Interface Segregation Principle轮傍,ISP)
- 合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle暂雹,CARP)
- 最少知識(shí)原則(Least Knowledge Principle,LKP)或者迪米特法則(Law of Demeter创夜,LOD)
設(shè)計(jì)模式原則名稱 | 簡(jiǎn)單定義 |
---|---|
開閉原則 | 對(duì)擴(kuò)展開放杭跪,對(duì)修改關(guān)閉 |
單一職責(zé)原則 | 一個(gè)類只負(fù)責(zé)一個(gè)功能領(lǐng)域中的相應(yīng)職責(zé) |
里氏代換原則 | 所有引用基類的地方必須能透明地使用其子類的對(duì)象 |
依賴倒轉(zhuǎn)原則 | 依賴于抽象,不能依賴于具體實(shí)現(xiàn) |
接口隔離原則 | 類之間的依賴關(guān)系應(yīng)該建立在最小的接口上 |
合成/聚合復(fù)用原則 | 盡量使用合成/聚合,而不是通過繼承達(dá)到復(fù)用的目的 |
迪米特法則 | 一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少的與其他實(shí)體發(fā)生相互作用 |
這個(gè)表格看起來(lái)有點(diǎn)抽象涧尿,下面逐條分析系奉。
開閉原則
開閉原則(Open Closed Principle,OCP)的定義是:一個(gè)軟件實(shí)體如類姑廉、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放缺亮,對(duì)修改關(guān)閉。模塊應(yīng)盡量在不修改原(是"原"桥言,指原來(lái)的代碼)代碼的情況下進(jìn)行擴(kuò)展萌踱。
開閉原則的意義:
在軟件的生命周期內(nèi),因?yàn)樽兓虐ⅰ⑸?jí)和維護(hù)等原因需要對(duì)軟件原有代碼進(jìn)行修改時(shí)并鸵,可能會(huì)給舊代碼中引入錯(cuò)誤,也可能會(huì)使我們不得不對(duì)整個(gè)功能進(jìn)行重構(gòu)扔涧,并且需要原有代碼經(jīng)過重新測(cè)試园担。當(dāng)軟件需要變化時(shí),盡量通過擴(kuò)展軟件實(shí)體的行為來(lái)實(shí)現(xiàn)變化枯夜,而不是通過修改已有的代碼來(lái)實(shí)現(xiàn)變化弯汰。
如何實(shí)現(xiàn)對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉卤档?
要實(shí)現(xiàn)對(duì)擴(kuò)展開放蝙泼,對(duì)修改關(guān)閉,即遵循開閉原則劝枣,需要對(duì)系統(tǒng)進(jìn)行抽象化設(shè)計(jì)汤踏,抽象可以基于抽象類或者接口。一般來(lái)說(shuō)需要做到幾點(diǎn):
- 1舔腾、通過接口或者抽象類約束擴(kuò)展溪胶,對(duì)擴(kuò)展進(jìn)行邊界限定,不允許出現(xiàn)在接口或抽象類中不存在的public方法稳诚,也就是擴(kuò)展必須添加具體實(shí)現(xiàn)而不是改變具體的方法哗脖。
- 2、參數(shù)類型扳还、引用對(duì)象盡量使用接口或者抽象類才避,而不是實(shí)現(xiàn)類,這樣就能盡量保證抽象層是穩(wěn)定的氨距。
- 3桑逝、一般抽象模塊設(shè)計(jì)完成(例如接口的方法已經(jīng)敲定),不允許修改接口或者抽象方法的定義俏让。
下面通過一個(gè)例子遵循開閉原則進(jìn)行設(shè)計(jì)楞遏,場(chǎng)景是這樣:某系統(tǒng)的后臺(tái)需要監(jiān)測(cè)業(yè)務(wù)數(shù)據(jù)展示圖表茬暇,如柱狀圖、折線圖等寡喝,在未來(lái)需要支持圖表的著色操作糙俗。在開始設(shè)計(jì)的時(shí)候,代碼可能是這樣的:
public class BarChart {
public void draw(){
System.out.println("Draw bar chart...");
}
}
public class LineChart {
public void draw(){
System.out.println("Draw line chart...");
}
}
public class App {
public void drawChart(String type){
if (type.equalsIgnoreCase("line")){
new LineChart().draw();
}else if (type.equalsIgnoreCase("bar")){
new BarChart().draw();
}
}
}
這樣做在初期是能滿足業(yè)務(wù)需要的预鬓,開發(fā)效率也十分高巧骚,但是當(dāng)后面需要新增一個(gè)餅狀圖的時(shí)候,既要添加一個(gè)餅狀圖的類珊皿,原來(lái)的客戶端App類的drawChart
方法也要新增一個(gè)if分支网缝,這樣做就是修改了原有客戶端類庫(kù)的方法,是十分不合理的蟋定。如果這個(gè)時(shí)候粉臊,在圖中加入一個(gè)顏色屬性,復(fù)雜性也大大提高驶兜《笾伲基于此,需要引入一個(gè)抽象Chart類AbstractChart抄淑,App類在畫圖的時(shí)候總是把相關(guān)的操作委托到具體的AbstractChart的派生類實(shí)例屠凶,這樣的話App類的代碼就不用修改:
public abstract class AbstractChart {
public abstract void draw();
}
public class BarChart extends AbstractChart{
@Override
public void draw() {
System.out.println("Draw bar chart...");
}
}
public class LineChart extends AbstractChart {
@Override
public void draw() {
System.out.println("Draw line chart...");
}
}
public class App {
public void drawChart(AbstractChart chart){
chart.draw();
}
}
如果新加一種圖,只需要新增一個(gè)AbstractChart的子類即可肆资〈@ⅲ客戶端類App不需要改變?cè)瓉?lái)的邏輯。修改后的設(shè)計(jì)符合開閉原則郑原,因?yàn)檎麄€(gè)系統(tǒng)在擴(kuò)展時(shí)原有的代碼沒有做任何修改。
單一職責(zé)原則
單一職責(zé)原則(Single Responsibility Principle, SRP)的定義是:指一個(gè)類或者模塊應(yīng)該有且只有一個(gè)改變的原因犯犁。如果一個(gè)類承擔(dān)的職責(zé)過多属愤,就等于把這些職責(zé)耦合在一起了。一個(gè)職責(zé)的變化可能會(huì)削弱或者抑制這個(gè)類完成其他職責(zé)的能力酸役。這種耦合會(huì)導(dǎo)致脆弱的設(shè)計(jì)住诸,當(dāng)發(fā)生變化時(shí),設(shè)計(jì)會(huì)遭受到意想不到的破壞涣澡。而如果想要避免這種現(xiàn)象的發(fā)生贱呐,就要盡可能的遵守單一職責(zé)原則。此原則的核心就是解耦和增強(qiáng)內(nèi)聚性入桂。
單一職責(zé)原則的意義:
單一職責(zé)原則告訴我們:一個(gè)類不能做太多的東西吼句。在軟件系統(tǒng)中,一個(gè)類(一個(gè)模塊事格、或者一個(gè)方法)承擔(dān)的職責(zé)越多惕艳,那么其被復(fù)用的可能性就會(huì)越低。一個(gè)很典型的例子就是萬(wàn)能類驹愚。其實(shí)可以說(shuō)一句大實(shí)話:任何一個(gè)常規(guī)的MVC項(xiàng)目远搪,在極端的情況下,可以用一個(gè)類(甚至一個(gè)方法)完成所有的功能逢捺。但是這樣做就會(huì)嚴(yán)重耦合谁鳍,甚至牽一發(fā)動(dòng)全身。一個(gè)類承(一個(gè)模塊劫瞳、或者一個(gè)方法)擔(dān)的職責(zé)過多倘潜,就相當(dāng)于將這些職責(zé)耦合在一起,當(dāng)其中一個(gè)職責(zé)變化時(shí)志于,可能會(huì)影響其他職責(zé)的運(yùn)作涮因,因此要將這些職責(zé)進(jìn)行分離,將不同的職責(zé)封裝在不同的類中伺绽,即將不同的變化原因封裝在不同的類中养泡,如果多個(gè)職責(zé)總是同時(shí)發(fā)生改變則可將它們封裝在同一類中。
不過說(shuō)實(shí)話奈应,其實(shí)有的時(shí)候很難去衡量一個(gè)類的職責(zé)澜掩,主要是很難確定職責(zé)的粒度。這一點(diǎn)不僅僅體現(xiàn)在一個(gè)類或者一個(gè)模塊中杖挣,也體現(xiàn)在采用微服務(wù)的分布式系統(tǒng)中肩榕。這也就是為什么我們?cè)趯?shí)施微服務(wù)拆分的時(shí)候經(jīng)常會(huì)撕逼:"這個(gè)功能不應(yīng)該發(fā)在A服務(wù)中,它不做這個(gè)領(lǐng)域的東西惩妇,應(yīng)該放在B服務(wù)中"諸如此類的爭(zhēng)論株汉。存在爭(zhēng)論是合理的,不過最好不要不了了之屿附,而應(yīng)該按照領(lǐng)域定義好每個(gè)服務(wù)的職責(zé)(職責(zé)的粒度最好找業(yè)務(wù)和架構(gòu)專家咨詢)郎逃,得出相對(duì)合理的職責(zé)分配。
下面通過一個(gè)很簡(jiǎn)單的實(shí)例說(shuō)明一下單一職責(zé)原則:
在一個(gè)項(xiàng)目系統(tǒng)代碼編寫的時(shí)候挺份,由于歷史原因和人為的不規(guī)范褒翰,導(dǎo)致項(xiàng)目沒有分層,一個(gè)Service類的偽代碼是這樣的:
public class Service {
public UserDTO findUser(String name){
Connection connection = getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");
preparedStatement.setObject(1, name);
User user = //處理結(jié)果
UserDTO dto = new UserDTO();
//entity值拷貝到dto
return dto;
}
}
這里出現(xiàn)一個(gè)問題匀泊,Service做了太多東西优训,包括數(shù)據(jù)庫(kù)連接的管理,Sql的執(zhí)行這些業(yè)務(wù)層不應(yīng)該接觸到的邏輯各聘,更可怕的是揣非,例如到時(shí)候如果數(shù)據(jù)庫(kù)換成了Oracle,這個(gè)方法將會(huì)大改躲因。因此早敬,拆分出新的DataBaseUtils類用于專門管理數(shù)據(jù)庫(kù)資源忌傻,Dao類用于專門執(zhí)行查詢和查詢結(jié)果封裝,改造后Service類的偽代碼如下:
public class Service {
private Dao dao;
public UserDTO findUser(String name){
User user = dao.findUserByName(name);
UserDTO dto = new UserDTO();
//entity值拷貝到dto
return dto;
}
}
public class Dao{
public User findUserByName(String name){
Connection connection = DataBaseUtils.getConnnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");
preparedStatement.setObject(1, name);
User user = //處理結(jié)果
return user;
}
}
現(xiàn)在搞监,如果有查詢封裝的變動(dòng)只需要修改Dao類水孩,數(shù)據(jù)庫(kù)相關(guān)變動(dòng)只需要修改DataBaseUtils類,每個(gè)類的職責(zé)分明琐驴。這個(gè)時(shí)候俘种,如果我們要把底層的存儲(chǔ)結(jié)構(gòu)緩成Redis或者M(jìn)ongoDB怎么辦,這樣顯然要重建整個(gè)Dao類绝淡,這種情況下宙刘,需要進(jìn)行接口隔離,下面分析接口隔離原則的時(shí)候再詳細(xì)分析牢酵。
里氏代換原則
里氏代換原則(Liskov Substitution Principle悬包,LSP)的定義是:所有引用基類的地方必須能透明地使用其子類的對(duì)象,也可以簡(jiǎn)單理解為任何基類可以出現(xiàn)的地方茁帽,子類一定可以出現(xiàn)玉罐。
里氏代換原則的意義:
只有當(dāng)衍生類可以替換掉基類,軟件單位的功能不受到影響時(shí)潘拨,基類才能真正被復(fù)用吊输,而衍生類也能夠在基類的基礎(chǔ)上增加新的行為。里氏代換原則是對(duì)"開-閉"原則的補(bǔ)充铁追。實(shí)現(xiàn)"開-閉"原則的關(guān)鍵步驟就是抽象化季蚂。而基類與子類的繼承關(guān)系就是抽象化的具體實(shí)現(xiàn),所以里氏代換原則是對(duì)實(shí)現(xiàn)抽象化的具體步驟的規(guī)范琅束。當(dāng)然扭屁,如果反過來(lái),軟件單位使用的是一個(gè)子類對(duì)象的話涩禀,那么它不一定能夠使用基類對(duì)象料滥。舉個(gè)很簡(jiǎn)單的例子說(shuō)明這個(gè)問題:如果一個(gè)方法接收Map類型參數(shù),那么它一定可以接收Map的子類參數(shù)例如HashMap艾船、LinkedHashMap葵腹、ConcurrentHashMap類型的參數(shù);但是返過來(lái)屿岂,如果另一個(gè)方法只接收HashMap類型的參數(shù)践宴,那么它一定不能接收所有Map類型的參數(shù),否則它可以接收LinkedHashMap爷怀、ConcurrentHashMap類型的參數(shù)阻肩。
子類為什么可以替換基類的位置?
其實(shí)原因很簡(jiǎn)單运授,只要存在繼承關(guān)系烤惊,基類的所有非私有屬性或者方法乔煞,子類都可以通過繼承獲得(白箱復(fù)用),反過來(lái)不成立撕氧,因?yàn)樽宇惡苡锌赡軘U(kuò)充自身的非私有屬性或者方法瘤缩,這個(gè)時(shí)候不能用基類獲取子類新增的這些屬性或者方法。
里氏代換原則是實(shí)現(xiàn)開閉原則的基礎(chǔ)伦泥,它告訴我們?cè)谠O(shè)計(jì)程序的時(shí)候進(jìn)可能使用基類進(jìn)行對(duì)象的定義和引用,在運(yùn)行時(shí)再?zèng)Q定基類的具體子類型锦溪。
舉個(gè)簡(jiǎn)單的例子不脯,假設(shè)一種會(huì)呼吸的動(dòng)物作為父類,子類豬和鳥也有自身的呼吸方式:
public abstract class Animal {
protected abstract void breathe();
}
public class Bird extends Animal {
@Override
public void breathe() {
System.out.println("Bird breathes...");
}
}
public class Pig extends Animal {
@Override
public void breathe() {
System.out.println("Pig breathes...");
}
}
public class App {
public static void main(String[] args) throws Exception {
Animal bird = new Bird();
bird.breathe();
Animal pig = new Pig();
pig.breathe();
}
}
依賴倒轉(zhuǎn)原則
依賴倒轉(zhuǎn)原則(Dependency Inversion Principle刻诊,DIP)的定義:程序要依賴于抽象接口防楷,不要依賴于具體實(shí)現(xiàn)。簡(jiǎn)單的說(shuō)就是要求對(duì)抽象進(jìn)行編程则涯,不要對(duì)實(shí)現(xiàn)進(jìn)行編程复局,這樣就降低了客戶與實(shí)現(xiàn)模塊間的耦合。
依賴倒轉(zhuǎn)原則的意義:
依賴倒轉(zhuǎn)原則要求我們?cè)诔绦虼a中傳遞參數(shù)時(shí)或在關(guān)聯(lián)關(guān)系中粟判,盡量引用層次高的抽象層類亿昏,即使用接口和抽象類進(jìn)行變量類型聲明、參數(shù)類型聲明档礁、方法返回類型聲明角钩,以及數(shù)據(jù)類型的轉(zhuǎn)換等,而不要用具體類來(lái)做這些事情呻澜。為了確保該原則的應(yīng)用递礼,一個(gè)具體類應(yīng)當(dāng)只實(shí)現(xiàn)接口或抽象類中聲明過的方法,而不要給出多余的方法羹幸,否則將無(wú)法調(diào)用到在子類中增加的新方法脊髓。在引入抽象層后,系統(tǒng)將具有很好的靈活性栅受,在程序中盡量使用抽象層進(jìn)行編程将硝,而將具體類寫在配置文件中,這樣一來(lái)窘疮,如果系統(tǒng)行為發(fā)生變化袋哼,只需要對(duì)抽象層進(jìn)行擴(kuò)展,并修改配置文件闸衫,而無(wú)須修改原有系統(tǒng)的源代碼涛贯,在不修改的情況下來(lái)擴(kuò)展系統(tǒng)的功能,滿足開閉原則的要求蔚出。
依賴倒轉(zhuǎn)原則的注意事項(xiàng):
- 高層模塊不應(yīng)該依賴低層模塊弟翘,高層模塊和低層模塊都應(yīng)該依賴于抽象虫腋。
- 抽象不應(yīng)該依賴于具體,具體應(yīng)該依賴于抽象稀余。
在實(shí)現(xiàn)依賴倒轉(zhuǎn)原則時(shí)悦冀,我們需要針對(duì)抽象層編程,而將具體類的對(duì)象通過依賴注入(DependencyInjection, DI)的方式注入到其他對(duì)象中睛琳,依賴注入是指當(dāng)一個(gè)對(duì)象要與其他對(duì)象發(fā)生依賴關(guān)系時(shí)盒蟆,通過抽象來(lái)注入所依賴的對(duì)象坏挠。常用的注入方式有三種澳淑,分別是:構(gòu)造注入,設(shè)值注入(Setter注入)和接口注入滥玷。Spring的IOC是此實(shí)現(xiàn)的典范辟癌。
從Java角度看待依賴倒轉(zhuǎn)原則的本質(zhì)就是:面向接口(抽象)編程寒屯。
- 每個(gè)具體的類都應(yīng)該有其接口或者基類,或者兩者都具備黍少。
- 類中的引用對(duì)象應(yīng)該是接口或者基類寡夹。
- 任何具體類都不應(yīng)該派生出子類。
- 盡量不要覆寫基類中的方法厂置。
- 結(jié)合里氏代換原則使用菩掏。
遵循依賴倒轉(zhuǎn)原則的一個(gè)例子,場(chǎng)景是司機(jī)開車:
public interface Driver {
void drive();
void setCar(Car car);
}
public interface Car {
void run();
}
public class DefaultDriver implements Driver {
private Car car;
@Override
public void drive() {
car.run();
}
@Override
public void setCar(Car car) {
this.car = car;
}
}
public class Bmw implements Car {
@Override
public void run() {
System.out.println("Bmw runs...");
}
}
public class Benz implements Car {
@Override
public void run() {
System.out.println("Benz runs...");
}
}
public class App {
public static void main(String[] args) throws Exception {
Driver driver = new DefaultDriver();
Car car = new Benz();
driver.setCar(car);
driver.drive();
car = new Bmw();
driver.setCar(car);
driver.drive();
}
}
這樣實(shí)現(xiàn)了一個(gè)司機(jī)可以開各種類型的車农渊,如果還有其他類型的車患蹂,只需要新加一個(gè)Car的實(shí)現(xiàn)即可。
接口隔離原則
接口隔離原則(Interface Segregation Principle砸紊,ISP)的定義是客戶端不應(yīng)該依賴它不需要的接口传于,類間的依賴關(guān)系應(yīng)該建立在最小的接口上。簡(jiǎn)單來(lái)說(shuō)就是建立單一的接口醉顽,不要建立臃腫龐大的接口沼溜。也就是接口盡量細(xì)化,同時(shí)接口中的方法盡量少游添。
如何看待接口隔離原則和單一職責(zé)原則系草?
單一職責(zé)原則注重的是類和接口的職責(zé)單一,這里職責(zé)是從業(yè)務(wù)邏輯上劃分的唆涝,但是在接口隔離原則要求當(dāng)一個(gè)接口太大時(shí)找都,我們需要將它分割成一些更細(xì)小的接口,使用該接口的客戶端僅需知道與之相關(guān)的方法即可廊酣。也就是說(shuō)能耻,我們?cè)谠O(shè)計(jì)接口的時(shí)候有可能滿足單一職責(zé)原則但是不滿足接口隔離原則。
接口隔離原則的規(guī)范:
- 使用接口隔離原則前首先需要滿足單一職責(zé)原則。
- 接口需要高內(nèi)聚晓猛,也就是提高接口饿幅、類、模塊的處理能力戒职,少對(duì)外發(fā)布public的方法栗恩。
- 定制服務(wù),就是單獨(dú)為一個(gè)個(gè)體提供優(yōu)良的服務(wù)洪燥,簡(jiǎn)單來(lái)說(shuō)就是拆分接口磕秤,對(duì)特定接口進(jìn)行定制。
- 接口設(shè)計(jì)是有限度的蚓曼,接口的設(shè)計(jì)粒度越小亲澡,系統(tǒng)越靈活,但是值得注意不能過小纫版,否則變成"字節(jié)碼編程"。
如果有用過spring-data-redis
的人就知道客情,RedisTemplate中持有一些列的基類其弊,分別是ValueOperations(處理K-V)、ListOperations(處理Hash)膀斋、SetOperations(處理集合)等等梭伐。
public interface ValueOperations<K, V> {
void set(K key, V value);
void set(K key, V value, long timeout, TimeUnit unit);
//....
}
合成/聚合復(fù)用原則
合成/聚合復(fù)用原則(Composite/Aggregate Reuse Principle,CARP)一般也叫合成復(fù)用原則(Composite Reuse Principle, CRP)仰担,定義是:盡量使用合成/聚合糊识,而不是通過繼承達(dá)到復(fù)用的目的。
合成/聚合復(fù)用原則就是在一個(gè)新的對(duì)象里面使用一些已有的對(duì)象摔蓝,使之成為新對(duì)象的一部分赂苗;新的對(duì)象通過向內(nèi)部持有的這些對(duì)象的委派達(dá)到復(fù)用已有功能的目的,而不是通過繼承來(lái)獲得已有的功能贮尉。
聚合(Aggregate)的概念:
聚合表示一種弱的"擁有"關(guān)系拌滋,一般表現(xiàn)為松散的整體和部分的關(guān)系,其實(shí)猜谚,所謂整體和部分也可以是完全不相關(guān)的败砂。例如A對(duì)象持有B對(duì)象,B對(duì)象并不是A對(duì)象的一部分魏铅,也就是B對(duì)象的生命周期是B對(duì)象自身管理昌犹,和A對(duì)象不相關(guān)。
合成(Composite)的概念:
合成表示一種強(qiáng)的"擁有"關(guān)系览芳,一般表現(xiàn)為嚴(yán)格的整體和部分的關(guān)系斜姥,部分和整體的生命周期是一樣的。
聚合和合成的關(guān)系:
這里用山羊舉例說(shuō)明聚合和合成的關(guān)系:
為什么要用合成/聚合來(lái)替代繼承達(dá)到復(fù)用的目的?
繼承復(fù)用破壞包裝,因?yàn)槔^承將基類的實(shí)現(xiàn)細(xì)節(jié)暴露給派生類疾渴,基類的內(nèi)部細(xì)節(jié)通常對(duì)子類來(lái)說(shuō)是可見的千贯,這種復(fù)用也稱為"白箱復(fù)用"。這里有一個(gè)明顯的問題是:派生類繼承自基類搞坝,如果基類的實(shí)現(xiàn)發(fā)生改變搔谴,將會(huì)影響到所有派生類的實(shí)現(xiàn);如果從基類繼承而來(lái)的實(shí)現(xiàn)是靜態(tài)的桩撮,不可能在運(yùn)行時(shí)發(fā)生改變敦第,不夠靈活。
由于合成或聚合關(guān)系可以將已有的對(duì)象店量,一般叫成員對(duì)象芜果,納入到新對(duì)象中,使之成為新對(duì)象的一部分融师,因此新對(duì)象可以調(diào)用已有對(duì)象的功能右钾,這樣做可以使得成員對(duì)象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)對(duì)于新對(duì)象不可見,所以這種復(fù)用又稱為"黑箱"復(fù)用旱爆,相對(duì)繼承關(guān)系而言舀射,其耦合度相對(duì)較低,成員對(duì)象的變化對(duì)新對(duì)象的影響不大怀伦,可以在新對(duì)象中根據(jù)實(shí)際需要有選擇性地調(diào)用成員對(duì)象的操作脆烟;合成/聚合復(fù)用可以在運(yùn)行時(shí)動(dòng)態(tài)進(jìn)行,新對(duì)象可以動(dòng)態(tài)地引用與成員對(duì)象類型相同的其他對(duì)象房待。
如果有閱讀過《Effective Java 2nd》的同學(xué)就知道邢羔,此書也建議慎用繼承。一般情況下桑孩,只有明確知道派生類和基類滿IS A
的時(shí)候才選用繼承拜鹤,當(dāng)滿足HAS A
或者不能判斷的情況下應(yīng)該選用合成/聚合。
下面舉個(gè)很極端的例子說(shuō)明一下如果在非IS A
的情況下使用繼承會(huì)出現(xiàn)什么問題:
先定義一個(gè)抽象手洼怔,手有一個(gè)搖擺的方法署惯,然后定義左右手繼承抽象手,實(shí)現(xiàn)搖擺方法:
public abstract class AbstractHand {
protected abstract void swing();
}
public class LeftHand extends AbstractHand {
@Override
public void swing() {
System.out.println("Left hand swings...");
}
}
public class RightHand extends AbstractHand {
@Override
public void swing() {
System.out.println("Right hand swings...");
}
}
現(xiàn)在看起來(lái)沒有任何問題镣隶,實(shí)現(xiàn)也十分正確极谊,現(xiàn)在出現(xiàn)了人(Person)這個(gè)類,具備搖左右手的功能安岂,如果不考慮IS A
的關(guān)系轻猖,很有可能有人會(huì)這樣做:
public abstract class AbstractSwingHand extends AbstractHand{
@Override
protected void swing() {
System.out.println(" hand swings...");
}
}
public class Person extends AbstractSwingHand {
public void swingLeftHand(){
System.out.print("Left ");
super.swing();
}
public void swingRightHand(){
System.out.print("Right ");
super.swing();
}
}
上面Person的實(shí)現(xiàn)讓人覺得百思不得其解,但是往往這會(huì)出現(xiàn)在真實(shí)的環(huán)境中域那,因?yàn)镠and不是Person咙边,所以Person繼承Hand一定會(huì)出現(xiàn)曲線實(shí)現(xiàn)等奇葩邏輯猜煮。Hand和Person是嚴(yán)格的部分和整體的關(guān)系,或者說(shuō)Person和Hand是HAS A
的關(guān)系败许,如果使用合成王带,邏輯將會(huì)十分清晰:
public class Person {
private AbstractHand leftHand;
private AbstractHand rightHand;
public Person() {
leftHand = new LeftHand();
rightHand = new RightHand();
}
public void swingLeftHand(){
leftHand.swing();
}
public void swingRightHand(){
rightHand.swing();
}
}
這里使用了合成,說(shuō)明了Person和AbstractHand實(shí)例的生命周期是一致的市殷。
迪米特法則
迪米特法則(Law of Demeter愕撰,LOD),有時(shí)候也叫做最少知識(shí)原則(Least Knowledge Principle醋寝,LKP)搞挣,它的定義是:一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用。每一個(gè)軟件單位對(duì)其他的單位都只有最少的知識(shí)音羞,而且局限于那些與本單位密切相關(guān)的軟件單位囱桨。迪米特法則的初衷在于降低類之間的耦合。由于每個(gè)類盡量減少對(duì)其他類的依賴嗅绰,因此舍肠,很容易使得系統(tǒng)的功能模塊功能獨(dú)立,相互之間不存在(或很少有)依賴關(guān)系窘面。迪米特法則不希望類之間建立直接的聯(lián)系貌夕。如果真的有需要建立聯(lián)系,也希望能通過它的友元類(中間類或者跳轉(zhuǎn)類)來(lái)轉(zhuǎn)達(dá)民镜。
迪米特法則的規(guī)則:
- Only talk to your immediate friends(只與直接的朋友通訊),一個(gè)對(duì)象的"朋友"包括他本身(this)险毁、它持有的成員對(duì)象制圈、入?yún)?duì)象、它所創(chuàng)建的對(duì)象畔况。
- 盡量少發(fā)布public的變量和方法鲸鹦,一旦公開的屬性和方法越多,修改的時(shí)候影響的范圍越大跷跪。
- "是自己的就是自己的"馋嗜,如果一個(gè)方法放在本類中,既不產(chǎn)生新的類間依賴吵瞻,也不造成負(fù)面的影響葛菇,那么次方法就應(yīng)該放在本類中。
迪米特法則的意義:
迪米特法則的核心觀念就是類間解耦橡羞,也就降低類之間的耦合眯停,只有類處于弱耦合狀態(tài),類的復(fù)用率才會(huì)提高卿泽。所謂降低類間耦合莺债,實(shí)際上就是盡量減少對(duì)象之間的交互,如果兩個(gè)對(duì)象之間不必彼此直接通信,那么這兩個(gè)對(duì)象就不應(yīng)當(dāng)發(fā)生任何直接的相互作用齐邦,如果其中的一個(gè)對(duì)象需要調(diào)用另一個(gè)對(duì)象的某一個(gè)方法的話椎侠,可以通過第三者轉(zhuǎn)發(fā)這個(gè)調(diào)用。簡(jiǎn)言之措拇,就是通過引入一個(gè)合理的第三者來(lái)降低現(xiàn)有對(duì)象之間的耦合度我纪。但是這樣會(huì)引發(fā)一個(gè)問題,有可能產(chǎn)生大量的中間類或者跳轉(zhuǎn)類儡羔,導(dǎo)致系統(tǒng)的復(fù)雜性提高宣羊,可維護(hù)性降低。如果一味追求極度解耦汰蜘,那么最終有可能變成面向字節(jié)碼編程甚至是面向二進(jìn)制的0和1編程仇冯。
舉個(gè)很簡(jiǎn)單的例子,體育老師要知道班里面女生的人數(shù)族操,他委托體育課代表點(diǎn)清女生的人數(shù):
public class Girl {
}
public class GroupLeader {
private final List<Girl> girls;
public GroupLeader(List<Girl> girls) {
this.girls = girls;
}
public void countGirls() {
System.out.println("The sum of girls is " + girls.size());
}
}
public class Teacher {
public void command(GroupLeader leader){
leader.countGirls();
}
}
public class App {
public static void main(String[] args) throws Exception {
Teacher teacher = new Teacher();
GroupLeader groupLeader = new GroupLeader(Arrays.asList(new Girl(), new Girl()));
teacher.command(groupLeader);
}
}
這個(gè)例子中苛坚,體育課代表就是中間類,體育課代表對(duì)于體育老師來(lái)說(shuō)就是"直接的朋友"色难,如果去掉體育課代表這個(gè)中間類泼舱,體育老師必須親自清點(diǎn)女生的人數(shù)(實(shí)際上就數(shù)人數(shù)這個(gè)功能,體育老師是不必要獲取所有女生的對(duì)象列表)枷莉,這樣做會(huì)違反迪米特法則娇昙。
小結(jié)
說(shuō)實(shí)話,設(shè)計(jì)模式的七大原則理解是比較困難的笤妙,我們?cè)谠O(shè)計(jì)模式的學(xué)習(xí)和應(yīng)用中經(jīng)常會(huì)聽到或者看到"XXX模式符合XXX原則"冒掌、"YYY模式不符合YYY原則"這樣的語(yǔ)句。因此蹲盘,為了分析設(shè)計(jì)模式的合理性和完善我們?nèi)粘5木幋a股毫,掌握和理解這七大原則是十分必要的。
參考
- 《Java設(shè)計(jì)模式》
- 《設(shè)計(jì)模式之禪-2nd》
- 《設(shè)計(jì)模式-可復(fù)用面向?qū)ο筌浖幕A(chǔ)》
(本文完)