設(shè)計模式(Design Pattern)是前輩們在代碼實踐中所總結(jié)的經(jīng)驗,是解決某些特定問題的套路拖刃。在使用一些優(yōu)秀的框架時兑牡,可能會接觸到它里面所運用到的一些設(shè)計模式,又或許你在編碼去設(shè)計一些模塊時税灌,為了提高代碼可復(fù)用性均函、擴展性亿虽、可讀性等,運用到的一些設(shè)計理念也會與某些設(shè)計模式思想相吻合苞也。
系統(tǒng)的了解和學(xué)習(xí)設(shè)計模式是很有必要的洛勉,能幫助提升面對對象設(shè)計的能力,了解各種設(shè)計模式的特點和運用場景
在學(xué)習(xí)設(shè)計模式前如迟,先了解下面對對象的設(shè)計原則
面對對象設(shè)計原則
對于一個好的面對對象軟件系統(tǒng)的設(shè)計來說收毫,可維護性和可復(fù)用性是很重要的,如何同時提高一個系統(tǒng)的可維護性和可復(fù)用性是面對對象設(shè)計需要解決的核心問題之一殷勘。
在面對對象設(shè)計中此再,面對對象設(shè)計原則是為了去支持可維護性和可復(fù)用性的,這些原則會體現(xiàn)在很多的設(shè)計模式中玲销,也就是說這些設(shè)計原則實際上就是從這些設(shè)計方案中總結(jié)提取出來的指導(dǎo)性原則输拇。
最常見的7種面向?qū)ο笤O(shè)計原則
設(shè)計原則名稱 | 定義 |
---|---|
開閉原則(Open-Closed Principle, OCP) | 軟件實體應(yīng)對擴展開放,而對修改關(guān)閉 |
單一職責(zé)原則(Single Responsibility Principle, SRP) | 一個類只負(fù)責(zé)一個功能領(lǐng)域中的相應(yīng)職責(zé) |
里氏代換原則(Liskov Substitution Principle, LSP) | 所有引用基類對象的地方能夠透明地使用其子類的對象 |
依賴倒轉(zhuǎn)原則(Dependence Inversion Principle, DIP) | 抽象不應(yīng)該依賴于細(xì)節(jié)贤斜,細(xì)節(jié)應(yīng)該依賴于抽象 |
接口隔離原則(Interface Segregation Principle, ISP) | 使用多個專門的接口策吠,而不使用單一的總接口 |
合成復(fù)用原則(Composite Reuse Principle,CRP) | 盡量使用對象組合,而不是繼承來達(dá)到復(fù)用的目的 |
迪米特法則(Law of Demeter, LoD) | 一個軟件實體應(yīng)當(dāng)盡可能少地與其他實體發(fā)生相互作用 |
設(shè)計原則
開閉原則
開閉原則(開放-封閉原則)有兩個特征蠢古,對擴展是開放的(Open for extension)奴曙,對修改是封閉的(Open for modification)。也就是說一個軟件實體(模塊草讶、類洽糟、函數(shù)等等)要實現(xiàn)變化,應(yīng)該是通過擴展而不是修改已有的代碼
任何的軟件在其生命周期內(nèi)需求都可能會發(fā)生變化堕战,既然變化是必然的坤溃,我們就應(yīng)該在設(shè)計時盡量適應(yīng)這些變化,以提高項目的穩(wěn)定性和靈活性嘱丢。如果一個軟件設(shè)計符合開閉原則薪介,那么可以非常方便地對系統(tǒng)進(jìn)行擴展,而且在擴展時無須修改現(xiàn)有代碼越驻,使得軟件系統(tǒng)在擁有適應(yīng)性和靈活性的同時具備較好的穩(wěn)定性和延續(xù)性汁政。隨著軟件規(guī)模越來越大,軟件壽命越來越長缀旁,軟件維護成本越來越高记劈,設(shè)計滿足開閉原則的軟件系統(tǒng)也變得越來越重要
為了滿足開閉原則,需要對系統(tǒng)進(jìn)行抽象化設(shè)計并巍,抽象化是開閉原則的關(guān)鍵目木。設(shè)計模塊時,對最可能發(fā)生變化的地方懊渡,通過構(gòu)造抽象來隔離這些變化刽射。在Java军拟、C#等編程語言中,可以為系統(tǒng)定義一個相對穩(wěn)定的抽象層誓禁,而將不同的實現(xiàn)行為移至具體的實現(xiàn)層中完成懈息。在很多面向?qū)ο缶幊陶Z言中都提供了接口、抽象類等機制现横,可以通過它們定義系統(tǒng)的抽象層漓拾,再通過具體類來進(jìn)行擴展。如果需要修改系統(tǒng)的行為戒祠,無須對抽象層進(jìn)行任何改動,只需要增加新的具體類來實現(xiàn)新的業(yè)務(wù)功能即可速种,實現(xiàn)在不修改已有代碼的基礎(chǔ)上擴展系統(tǒng)的功能姜盈,達(dá)到開閉原則的要求
這里舉一個簡單的例子,某個系統(tǒng)中某個功能可以來顯示各種類型的圖表配阵,比如餅圖和柱狀圖馏颂。開始的設(shè)計方案如下:
ChartDisplay中的display方法如下
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
在這個例子中,假如我需要添加新的圖表對象(折線圖LineChart)棋傍,那么我需要在ChartDisplay中的display方法中去添加新的判斷邏輯救拉,這是不符合開閉原則。ChartDisplay類是用來做圖表的顯示工作瘫拣,但具體的圖表是變化的亿絮,需要將這些變化隔離出來
抽象化的方法:
- 增加一個抽象類AbstractChart,作為其他具體圖表類的父類
- ChartDisplay的display方法只針對抽象父類AbstractChart麸拄,而具體的圖表類交由客戶端去選擇
重構(gòu)后的結(jié)構(gòu)如下
如上派昧,ChartDisplay只針對抽象類AbstractChart編程,通過setChart來獲得具體的圖表對象拢切,dispalay方法中直接執(zhí)行 chart.display()蒂萎,當(dāng)我們要新增新的圖表,那么直接創(chuàng)建圖表子類繼承AbstractChart淮椰,并實現(xiàn)自己的display方法就好五慈,并不需要修改已有的代碼。
單一職責(zé)原則
單一職責(zé)原則(Single Responsibility Principle, SRP):一個類應(yīng)該只有一個職責(zé)主穗,對外只提供一種功能泻拦,應(yīng)該有且僅有一個原因引起類的變化
能力越大,責(zé)任越大黔牵?我們不能創(chuàng)建一個“超級類”聪轿,能解決所有的事情,相反猾浦,一個類(大到模塊陆错,小到方法)所承擔(dān)的責(zé)任越多灯抛,那么他被復(fù)用的可能性就越小。而且一個類承擔(dān)的職責(zé)過多音瓷,這些職責(zé)耦合度會很高对嚼,當(dāng)其中一個職責(zé)變化時,可能會影響其他職責(zé)的運作绳慎,因此要將這些職責(zé)進(jìn)行分離纵竖,將不同的職責(zé)封裝在不同的類中,將不同的變化原因封裝在不同的類中杏愤,如果多個職責(zé)總是同時發(fā)生改變則可將它們封裝在同一類中
單一職責(zé)原則靡砌,用于控制類的粒度大小,實現(xiàn)高內(nèi)聚珊楼、低耦合通殃,它是最簡單但又最難運用的原則,如何發(fā)現(xiàn)類的不同職責(zé)并將其分離厕宗,需要具有較強的分析設(shè)計能力和相關(guān)實踐經(jīng)驗画舌。如果你能夠想到多于一個動機去改變一個類,那么這個類就有多于一個的職責(zé)已慢,就要考慮類的職責(zé)分離
記得在剛?cè)腴TJava接觸到 JDBC的時候曲聂,為了實現(xiàn)查詢學(xué)生列表,一口氣從數(shù)據(jù)庫的連接到數(shù)據(jù)查詢再到數(shù)據(jù)展示佑惠,簡直“一氣呵成”朋腋,但這種面向過程式的編程卻沒有很好的擴展性,當(dāng)我想要再實現(xiàn)其他功能時兢仰,將會有大量重復(fù)的代碼乍丈,而重復(fù)的地方需要修改,那就更麻煩了把将。后來稍微改進(jìn)了轻专,建立了只負(fù)責(zé)數(shù)據(jù)庫連接資源的類DBUtil,再到后來使用持久層的框架察蹲。職責(zé)劃分后请垛,開發(fā)時便只需關(guān)注業(yè)務(wù)的處理
單一職責(zé)適用于接口、類洽议,同時也適用于方法宗收,一個方法盡可能做一件事情,比如一個方法修改用戶密碼亚兄,不要把這個方法放到“修改用戶信息”方法中混稽,這個方法的顆粒度很粗
上面的方法就職責(zé)不清晰,不單一,下面替換成具體的修改動作匈勋,通過命名我們就能知曉方法的大概處理邏輯
里式替換原則
里氏代換原則(Liskov Substitution Principle, LSP):所有引用基類(父類)的地方必須能透明地使用其子類的對象
里氏代換原則告訴我們礼旅,在軟件中,只要父類能出現(xiàn)的地方子類就可以出現(xiàn),而且替換為子類也不會產(chǎn)生任何錯誤或異常洽洁,程序?qū)⒉粫a(chǎn)生任何錯誤和異常痘系,反過來則不成立,如果一個軟件實體使用的是一個子類對象的話饿自,那么它不一定能夠使用基類對象
里式替換才使得開發(fā)-封閉成為可能汰翠,子類的可替代性才使得使用父類類型的地方可以在無需修改的情況下就可以擴展。里氏代換原則是實現(xiàn)開閉原則的重要方式之一昭雌,由于使用基類對象的地方都可以使用子類對象复唤,因此在程序中盡量使用基類類型來對對象進(jìn)行定義,而在運行時再確定其子類類型烛卧,用子類對象來替換父類對象
- 子類的所有方法必須在父類中聲明苟穆,或子類必須實現(xiàn)父類中聲明的所有方法。根據(jù)里氏代換原則唱星,為了保證系統(tǒng)的擴展性,在程序中通常使用父類來進(jìn)行定義跟磨,如果一個方法只存在子類中间聊,在父類中不提供相應(yīng)的聲明,則無法在以父類定義的對象中使用該方法抵拘。
- 我們在運用里氏代換原則時哎榴,盡量把父類設(shè)計為抽象類或者接口,讓子類繼承父類或?qū)崿F(xiàn)父接口僵蛛,并實現(xiàn)在父類中聲明的方法尚蝌,運行時,子類實例替換父類實例充尉,我們可以很方便地擴展系統(tǒng)的功能飘言,同時無須修改原有子類的代碼,增加新的功能可以通過增加一個新的子類來實現(xiàn)驼侠。里氏代換原則是開閉原則的具體實現(xiàn)手段之一
依賴倒轉(zhuǎn)原則
如果說開閉原則是面向?qū)ο笤O(shè)計的目標(biāo)的話姿鸿,那么依賴倒轉(zhuǎn)原則就是面向?qū)ο笤O(shè)計的主要實現(xiàn)機制之一,它是系統(tǒng)抽象化的具體實現(xiàn)
依賴倒轉(zhuǎn)原則(Dependency Inversion Principle, DIP):高層模塊不應(yīng)該依賴低層模塊倒源,兩者都應(yīng)該依賴其抽象苛预;抽象不應(yīng)該依賴細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象
上面的定義有些別扭笋熬,引入《設(shè)計模式之禪》的話來說明依賴倒轉(zhuǎn)
高層模塊和低層模塊容易理解热某,每一個邏輯的實現(xiàn)都是由原子邏輯組成的,不可分割的原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊昔馋。那什么是抽象筹吐?什么又是細(xì)節(jié)呢?在Java語言中绒极,抽象就是指接口或抽象類骏令,兩者都是不能直接被實例化的;細(xì)節(jié)就是實現(xiàn)類垄提,實現(xiàn)接口或繼承抽象類而產(chǎn)生的類就是細(xì)節(jié)榔袋,其特點就是可以直接被實例化,也就是可以加上一個關(guān)鍵字new產(chǎn)生一個對象铡俐。
依賴倒置原則在Java語言中的表現(xiàn)就是:
- 模塊間的依賴通過抽象發(fā)生凰兑,實現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的审丘;
- 接口或抽象類不依賴于實現(xiàn)類吏够;
- 實現(xiàn)類依賴接口或抽象類
更精簡的定義就是要面向接口編程(Object-Oriented Design),而不是針對實現(xiàn)編程
看到依賴倒轉(zhuǎn)和它的定義滩报,是否會想起Spring的依賴注入(Dependency Injection, DI)和控制反轉(zhuǎn)(Inversion of Control,IOC)锅知,通常我們使用Spring的IoC容器時,會聲明依賴的接口脓钾,在程序運行時確定具體的實現(xiàn)類并注入售睹。這樣便降低了類間的耦合性、提高了系統(tǒng)的穩(wěn)定性
接口分離原則
接口隔離原則(Interface Segregation Principle, ISP):使用多個專門的接口可训,而不使用單一的總接口昌妹,
即客戶端不應(yīng)該依賴那些它不需要的接口
根據(jù)接口隔離原則,當(dāng)一個接口太大時握截,我們需要將它分割成一些更細(xì)小的接口飞崖,使用該接口的客戶端僅需知道與之相關(guān)的方法即可。每一個接口應(yīng)該承擔(dān)一種相對獨立的角色谨胞,不干不該干的事固歪,該干的事都要干。這里的“接
口”往往有兩種不同的含義:一種是指一個類型所具有的方法特征的集合畜眨,僅僅是一種邏輯上的抽象昼牛;另外一種是指某種語言具體的“接口”定義,有嚴(yán)格的定義和結(jié)構(gòu)康聂,比如Java語言中的interface贰健。對于這兩種不同的含義,ISP的表達(dá)方式以及含義都有所不同:
(1) 當(dāng)把“接口”理解成一個類型所提供的所有方法特征的集合的時候恬汁,這就是一種邏輯上的概念伶椿,接口的劃分將直接帶來類型的劃分辜伟。可以把接口理解成角色脊另,一個接口只能代表一個角色导狡,每個角色都有它特定的一個接口,此時偎痛,這個原則可以叫做“角色隔離原則”旱捧。
(2) 如果把“接口”理解成狹義的特定語言的接口,那么ISP表達(dá)的意思是指接口僅僅提供客戶端需要的行為踩麦,客戶端不需要的行為則隱藏起來枚赡,應(yīng)當(dāng)為客戶端提供盡可能小的單獨的接口,而不要提供大的總接口谓谦。在面向?qū)ο缶幊陶Z言中贫橙,實現(xiàn)一個接口就需要實現(xiàn)該接口中定義的所有方法,因此大的總接口使用起來不一定很方便反粥,為了使接口的職責(zé)單一卢肃,需要將大接口中的方法根據(jù)其職責(zé)不同分別放在不同的小接口中,以確保每個接口使用起來都較為方便才顿,并都承擔(dān)某一單一角色莫湘。接口應(yīng)該盡量細(xì)化,同時接口中的方法應(yīng)該盡量少郑气,每個接口中只包含一個客戶(如子模塊或業(yè)務(wù)邏輯類)所需的方法即可逊脯,這種機制也稱為“定制服務(wù)”,即為不同的客戶端提供寬窄不同的接口竣贪。
接口隔離原則和單一職責(zé)都是為了提高類的內(nèi)聚性、降低它們之間的耦合性巩螃,體現(xiàn)了封裝的思想演怎,但兩者是不同的:
- 單一職責(zé)原則注重的是職責(zé),而接口隔離原則注重的是對接口依賴的隔離
- 單一職責(zé)原則主要是約束類避乏,它針對的是程序中的實現(xiàn)和細(xì)節(jié)爷耀;接口隔離原則主要約束接口,主要針對抽象和程序整體框架的構(gòu)建
合成復(fù)用原則
合成復(fù)用原則又稱為組合/聚合復(fù)用原則(Composition/Aggregate Reuse Principle, CARP)
合成復(fù)用原則(Composite Reuse Principle, CRP):盡量使用對象組合拍皮,而不是繼承來達(dá)到復(fù)用的目的
合成復(fù)用原則就是在一個新的對象里通過關(guān)聯(lián)關(guān)系(包括組合關(guān)系和聚合關(guān)系)來使用一些已有的對象歹叮,使之成為新對象的一部分;新對象通過委派調(diào)用已有對象的方法達(dá)到復(fù)用功能的目的铆帽。簡言之:復(fù)用時要盡量使用組合/聚合關(guān)系(關(guān)聯(lián)關(guān)系)咆耿,少用繼承
在面向?qū)ο笤O(shè)計中,可以通過兩種方法在不同的環(huán)境中復(fù)用已有的設(shè)計和實現(xiàn)爹橱,即通過組合/聚合關(guān)系或通過繼承萨螺,但首先應(yīng)該考慮使用組合/聚合,組合/聚合可以使系統(tǒng)更加靈活,降低類與類之間的耦合度慰技,一個類的變化對其他類造成的影響相對較少椭盏;其次才考慮繼承,在使用繼承時吻商,需要嚴(yán)格遵循里氏代換原則掏颊,有效使用繼承會有助于對問題的理解,降低復(fù)雜度艾帐,而濫用繼承反而會增加系統(tǒng)構(gòu)建和維護的難度以及系統(tǒng)的復(fù)雜度乌叶,因此需要慎重使用繼承復(fù)用
繼承復(fù)用的主要問題在于繼承復(fù)用會破壞系統(tǒng)的封裝性:
- 因為繼承會將基類的實現(xiàn)細(xì)節(jié)暴露給子類,由于基類的內(nèi)部細(xì)節(jié)通常對子類來說是可見的掩蛤,所以這種復(fù)用又稱“白箱”復(fù)用
- 子類與父類的耦合度高枉昏,如果父類發(fā)生改變,那么子類的實現(xiàn)也不得不發(fā)生改變揍鸟,這不利于類的擴展與維護
- 從父類繼承而來的實現(xiàn)是靜態(tài)的兄裂,不可能在運行時發(fā)生改變,沒有足夠的靈活性
- 而且繼承只能在有限的環(huán)境中使用(如類沒有聲明為不能被繼承)
組合或聚合關(guān)系可以將已有的對象到新對象中阳藻,使之成為新對象的一部分
- 新對象可以調(diào)用已有對象的功能晰奖,這樣做可以使得成員對象的內(nèi)部實現(xiàn)細(xì)節(jié)對于新對象不可見,所以這種復(fù)用又稱為“黑箱”復(fù)用
- 相對繼承關(guān)系而言腥泥,其耦合度相對較低匾南,成員對象的變化對新對象的影響不大,可以在新對象中根據(jù)實際需要有選擇性地調(diào)用成員對象的操作
- 合成復(fù)用可以在運行時動態(tài)進(jìn)行蛔外,新對象可以動態(tài)地引用與成員對象類型相同的其他對象
一般而言蛆楞,如果兩個類之間是“Has-A”的關(guān)系應(yīng)使用組合或聚合,如果是“Is-A”關(guān)系可使用繼承夹厌。"Is-A"是嚴(yán)格的分類學(xué)意義上的定義豹爹,意思是一個類是另一個類的"一種";而"Has-A"則不同矛纹,它表示某一個角色具有某一項責(zé)任臂聋。
迪米特原則
迪米特法則(Law of Demeter,LoD)也稱為最少知識原則(Least Knowledge Principle或南,LKP)
迪米特法則(Law of Demeter, LoD):一個軟件實體應(yīng)當(dāng)盡可能少地與其他實體發(fā)生相互作用
如果一個系統(tǒng)符合迪米特法則孩等,那么當(dāng)其中某一個模塊發(fā)生修改時,就會盡量少地影響其他模塊采够,擴展會相對容易肄方,這是對軟件實體之間通信的限制,迪米特法則要求限制軟件實體之間通信的寬度和深度蹬癌。迪米特法則可降低系統(tǒng)的耦合度扒秸,使類與類之間保持松散的耦合關(guān)系播演。
迪米特法則還有幾種定義形式,包括:不要和“陌生人”說話伴奥、只與你的直接朋友通信等写烤,在迪米特法則中,對于一個對象拾徙,其朋友包括以下幾類:
當(dāng)前對象本身(this)洲炊;
以參數(shù)形式傳入到當(dāng)前對象方法中的對象;
當(dāng)前對象的成員對象尼啡;
如果當(dāng)前對象的成員對象是一個集合暂衡,那么集合中的元素也都是朋友;
當(dāng)前對象所創(chuàng)建的對象崖瞭。
任何一個對象狂巢,如果滿足上面的條件之一,就是當(dāng)前對象的“朋友”书聚,否則就是“陌生人”唧领。在應(yīng)用迪米特法則時,一個對象只能與直接朋友發(fā)生交互雌续,不要與“陌生人”發(fā)生直接交互斩个,這樣做可以降低系統(tǒng)的耦合度,一個對象的改變不會給太多其他對象帶來影響驯杜。
迪米特法則要求我們在設(shè)計系統(tǒng)時受啥,應(yīng)該盡量減少對象之間的交互,如果兩個對象之間不必彼此直接通信鸽心,那么這兩個對象就不應(yīng)當(dāng)發(fā)生任何直接的相互作用滚局,如果其中的一個對象需要調(diào)用另一個對象的某一個方法的話,可以通過第三者轉(zhuǎn)發(fā)這個調(diào)用顽频。簡言之核畴,就是通過引入一個合理的第三者來降低現(xiàn)有對象之間的耦合度。
在將迪米特法則運用到系統(tǒng)設(shè)計中時冲九,要注意下面的幾點:
- 在類的劃分上,應(yīng)當(dāng)盡量創(chuàng)建松耦合的類跟束,類之間的耦合度越低莺奸,就越有利于復(fù)用,一個處在松耦合中的類一旦被修改冀宴,不會對關(guān)聯(lián)的類造成太大波及
- 在類的結(jié)構(gòu)設(shè)計上灭贷,每一個類都應(yīng)當(dāng)盡量降低其成員變量和成員函數(shù)的訪問權(quán)限
- 在類的設(shè)計上,只要有可能略贮,一個類型應(yīng)當(dāng)設(shè)計成不變類
- 在對其他類的引用上甚疟,一個對象對其他對象的引用應(yīng)當(dāng)降到最低
總結(jié)
這 7 種設(shè)計原則是軟件設(shè)計模式必須盡量遵循的原則仗岖,各種原則要求的側(cè)重點不同。
- 開閉原則是總綱览妖,它告訴我們要【對擴展開放轧拄,對修改關(guān)閉】
- 里氏替換原則告訴我們【不要破壞繼承體系】
- 依賴倒置原則告訴我們要【面向接口編程】
- 單一職責(zé)原則告訴我們實現(xiàn)類要【職責(zé)單一】
- 接口隔離原則告訴我們在設(shè)計接口的時候要【精簡單一】
- 迪米特法則告訴我們要【降低耦合度】
- 合成復(fù)用原則告訴我們要【優(yōu)先使用組合或者聚合關(guān)系復(fù)用,少用繼承關(guān)系復(fù)用】
23種設(shè)計模式
總體來說讽膏,設(shè)計模式按照功能分為三類23種:
- 創(chuàng)建型(5種) : 工廠模式檩电、抽象工廠模式、單例模式府树、原型模式俐末、建造者模式
- 結(jié)構(gòu)型(7種): 適配器模式、裝飾模式奄侠、代理模式 卓箫、外觀模式、橋接模式垄潮、組合模式烹卒、享元模式
- 行為型(11種): 模板方法模式、策略模式 魂挂、觀察者模式甫题、中介者模式、狀態(tài)模式涂召、責(zé)任鏈模式坠非、命令模式、迭代器模式果正、訪問者模式砌们、解釋器模式担租、備忘錄模式
參考:《大話設(shè)計模式》、《設(shè)計模式之禪》、網(wǎng)上相關(guān)設(shè)計模式文章