學(xué)習(xí)并理解 23 種設(shè)計(jì)模式
設(shè)計(jì)模式 Design Pattern 是一套被反復(fù)使用、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié),使用設(shè)計(jì)模式是為了可重用代碼、讓代碼更容易被他人理解并且保證代碼可靠性。。
在《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書中所介紹的 23 種經(jīng)典設(shè)計(jì)模式,不過設(shè)計(jì)模式并不僅僅只有這 23 種,隨著軟件開發(fā)行業(yè)的發(fā)展,越來越多的新模式不斷誕生并得以應(yīng)用。有經(jīng)驗(yàn)的開發(fā)者在學(xué)習(xí)設(shè)計(jì)模式可以和過往的經(jīng)驗(yàn)互相印證,更容易理解這些設(shè)計(jì)模式。
設(shè)計(jì)模式一般包含模式名稱、問題、目的、解決方案、效果等組成要素。問題描述了應(yīng)該在何時(shí)使用模式,它包含了設(shè)計(jì)中存在的問題以及問題存在的原因。解決方案描述了一個設(shè)計(jì)模式的組成成分,以及這些組成成分之間的相互關(guān)系,各自的職責(zé)和協(xié)作方式,通常解決方案通過 UML 類圖和核心代碼來進(jìn)行描述。效果描述了模式的優(yōu)缺點(diǎn)以及在使用模式時(shí)應(yīng)權(quán)衡的問題。
為什么要學(xué)習(xí)設(shè)計(jì)模式:
設(shè)計(jì)模式來源眾多專家的經(jīng)驗(yàn)和智慧,它們是從許多優(yōu)秀的軟件系統(tǒng)中總結(jié)出的成功的、能夠?qū)崿F(xiàn)可維護(hù)性復(fù)用的設(shè)計(jì)方案,使用這些方案將可以讓我們避免做一些重復(fù)性的工作
設(shè)計(jì)模式提供了一套通用的設(shè)計(jì)詞匯和一種通用的形式來方便開發(fā)人員之間溝通和交流,使得設(shè)計(jì)方案更加通俗易懂
大部分設(shè)計(jì)模式都兼顧了系統(tǒng)的可重用性和可擴(kuò)展性,這使得我們可以更好地重用一些已有的設(shè)計(jì)方案、功能模塊甚至一個完整的軟件系統(tǒng),避免我們經(jīng)常做一些重復(fù)的設(shè)計(jì)、編寫一些重復(fù)的代碼
合理使用設(shè)計(jì)模式并對設(shè)計(jì)模式的使用情況進(jìn)行文檔化,將有助于別人更快地理解系統(tǒng)
學(xué)習(xí)設(shè)計(jì)模式將有助于初學(xué)者更加深入地理解面向?qū)ο笏枷?/p>
儲備知識:
抽象類:一般抽象類都是作為基類,比如說「電腦」就可以作為一個抽象類,根據(jù)抽象類派生出「臺式電腦」和「筆記本電腦」2種具體類。一般不對抽象類進(jìn)行實(shí)例化。
組合優(yōu)于繼承:不能濫用繼承來拓展功能,配合組合會更靈活。同樣拿「電腦」抽象類來舉例,如果使用繼承,區(qū)分不同類型的「電腦」我們可以派生出「臺式電腦」和「筆記本電腦」,如果再增加一個維度,根據(jù)品牌又能繼續(xù)細(xì)分出「聯(lián)想臺式電腦」、「聯(lián)想筆記本電腦」、「蘋果臺式電腦」和「蘋果筆記本電腦」等等,如果再增加一個維度繼續(xù)細(xì)分下去,顯然繼承是無法勝任的。這個時(shí)候可以使用繼承加組合方式,組合的對象也可以進(jìn)行抽象化設(shè)計(jì):
一、UML 類圖
每個模式都有相應(yīng)的對象結(jié)構(gòu)圖,同時(shí)為了展示對象間的交互細(xì)節(jié), 有些時(shí)候會用到 UML 圖來介紹其如何運(yùn)行。這里不會將 UML 的各種元素都提到,只想講講類圖中各個類之間的關(guān)系, 能看懂類圖中各個類之間的線條、箭頭代表什么意思后,也就足夠應(yīng)對日常的工作和交流。同時(shí),我們應(yīng)該能將類圖所表達(dá)的含義和最終的代碼對應(yīng)起來。有了這些知識,看后面章節(jié)的設(shè)計(jì)模式結(jié)構(gòu)圖就沒有什么問題了。
本文中大部分是 UML 類圖,也有個別簡易流程圖。由于文中部分模式并未配圖,你可以在這里查看我在網(wǎng)絡(luò)上收集的完整 23 種設(shè)計(jì)模式 UML 類圖。
1.1 繼承
繼承用一條帶空心箭頭的直接表示。
1.2 實(shí)現(xiàn)
實(shí)現(xiàn)關(guān)系用一條帶空心箭頭的虛線表示。
1.3 組合
與聚合關(guān)系一樣,組合關(guān)系同樣表示整體由部分構(gòu)成的語義。比如公司由多個部門組成,但組合關(guān)系是一種強(qiáng)依賴的特殊聚合關(guān)系,如果整體不存在了,則部分也不存在了。例如,公司不存在了,部門也將不存在了。
1.4 聚合
聚合關(guān)系用于表示實(shí)體對象之間的關(guān)系,表示整體由部分構(gòu)成的語義,例如一個部門由多個員工組成。與組合關(guān)系不同的是,整體和部分不是強(qiáng)依賴的,即使整體不存在了,部分仍然存在。例如,部門撤銷了,人員不會消失,他們依然存在。
1.5 關(guān)聯(lián)
關(guān)聯(lián)關(guān)系是用一條直線表示的,它描述不同類的對象之間的結(jié)構(gòu)關(guān)系,它是一種靜態(tài)關(guān)系, 通常與運(yùn)行狀態(tài)無關(guān),一般由常識等因素決定的。它一般用來定義對象之間靜態(tài)的、天然的結(jié)構(gòu), 所以,關(guān)聯(lián)關(guān)系是一種“強(qiáng)關(guān)聯(lián)”的關(guān)系。
比如,乘車人和車票之間就是一種關(guān)聯(lián)關(guān)系,學(xué)生和學(xué)校就是一種關(guān)聯(lián)關(guān)系,關(guān)聯(lián)關(guān)系默認(rèn)不強(qiáng)調(diào)方向,表示對象間相互知道。如果特別強(qiáng)調(diào)方向,如下圖,表示 A 知道 B ,但 B 不知道 A 。
1.6 依賴
依賴關(guān)系是用一套帶箭頭的虛線表示的,如A依賴于B,他描述一個對象在運(yùn)行期間會用到另一個對象的關(guān)系。
與關(guān)聯(lián)關(guān)系不同的是,它是一種臨時(shí)性的關(guān)系,通常在運(yùn)行期間產(chǎn)生,并且隨著運(yùn)行時(shí)的變化,依賴關(guān)系也可能發(fā)生變化。顯然,依賴也有方向,雙向依賴是一種非常糟糕的結(jié)構(gòu),我們總是應(yīng)該保持單向依賴,杜絕雙向依賴的產(chǎn)生。
二、六大原則
2.1 開閉原則
一個軟件實(shí)體應(yīng)當(dāng)對擴(kuò)展開放,對修改關(guān)閉。即軟件實(shí)體應(yīng)盡量在不修改原有代碼的情況下進(jìn)行擴(kuò)展。
任何軟件都需要面臨一個很重要的問題,即它們的需求會隨時(shí)間的推移而發(fā)生變化。當(dāng)軟件系統(tǒng)需要面對新的需求時(shí),我們應(yīng)該盡量保證系統(tǒng)的設(shè)計(jì)框架是穩(wěn)定的。如果一個軟件設(shè)計(jì)符合開閉原則,那么可以非常方便地對系統(tǒng)進(jìn)行擴(kuò)展,而且在擴(kuò)展時(shí)無須修改現(xiàn)有代碼,使得軟件系統(tǒng)在擁有適應(yīng)性和靈活性的同時(shí)具備較好的穩(wěn)定性和延續(xù)性。隨著軟件規(guī)模越來越大,軟件壽命越來越長,軟件維護(hù)成本越來越高,設(shè)計(jì)滿足開閉原則的軟件系統(tǒng)也變得越來越重要。
為了滿足開閉原則,需要對系統(tǒng)進(jìn)行抽象化設(shè)計(jì),抽象化是開閉原則的關(guān)鍵。在Java、C#等編程語言中,可以為系統(tǒng)定義一個相對穩(wěn)定的抽象層,而將不同的實(shí)現(xiàn)行為移至具體的實(shí)現(xiàn)層中完成。在很多面向?qū)ο缶幊陶Z言中都提供了接口、抽象類等機(jī)制,可以通過它們定義系統(tǒng)的抽象層,再通過具體類來進(jìn)行擴(kuò)展。如果需要修改系統(tǒng)的行為,無須對抽象層進(jìn)行任何改動,只需要增加新的具體類來實(shí)現(xiàn)新的業(yè)務(wù)功能即可,實(shí)現(xiàn)在不修改已有代碼的基礎(chǔ)上擴(kuò)展系統(tǒng)的功能,達(dá)到開閉原則的要求。
優(yōu)點(diǎn):實(shí)踐開閉原則的優(yōu)點(diǎn)在于可以在不改動原有代碼的前提下給程序擴(kuò)展功能。增加了程序的可擴(kuò)展性,同時(shí)也降低了程序的維護(hù)成本。
2.2 里氏替換原則
所有引用基類對象的地方能夠透明地使用其子類的對象
里氏代換原則告訴我們,在軟件中將一個基類對象替換成它的子類對象,程序?qū)⒉粫a(chǎn)生任何錯誤和異常,反過來則不成立,如果一個軟件實(shí)體使用的是一個子類對象的話,那么它不一定能夠使用基類對象。例如:我喜歡動物,那我一定喜歡狗,因?yàn)楣肥莿游锏淖宇?。但是我喜歡狗,不能據(jù)此斷定我喜歡動物,因?yàn)槲也⒉幌矚g老鼠,雖然它也是動物。
例如有兩個類,一個類為BaseClass,另一個是SubClass類,并且SubClass類是BaseClass類的子類,那么一個方法如果可以接受一個BaseClass類型的基類對象base的話,如:method1(base),那么它必然可以接受一個BaseClass類型的子類對象sub,method1(sub)能夠正常運(yùn)行。反過來的代換不成立,如一個方法method2接受BaseClass類型的子類對象sub為參數(shù):method2(sub),那么一般而言不可以有method2(base),除非是重載方法。
里氏代換原則是實(shí)現(xiàn)開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,因此在程序中盡量使用基類類型來對對象進(jìn)行定義,而在運(yùn)行時(shí)再確定其子類類型,用子類對象來替換父類對象。
優(yōu)點(diǎn):可以檢驗(yàn)繼承使用的正確性,約束繼承在使用上的泛濫。
2.3 依賴倒置原則
抽象不應(yīng)該依賴于具體類,具體類應(yīng)當(dāng)依賴于抽象。換言之,要針對接口編程,而不是針對實(shí)現(xiàn)編程。
依賴倒轉(zhuǎn)原則要求我們在程序代碼中傳遞參數(shù)時(shí)或在關(guān)聯(lián)關(guān)系中,盡量引用層次高的抽象層類,即使用接口和抽象類進(jìn)行變量類型聲明、參數(shù)類型聲明、方法返回類型聲明,以及數(shù)據(jù)類型的轉(zhuǎn)換等,而不要用具體類來做這些事情。為了確保該原則的應(yīng)用,一個具體類應(yīng)當(dāng)只實(shí)現(xiàn)接口或抽象類中聲明過的方法,而不要給出多余的方法,否則將無法調(diào)用到在子類中增加的新方法。
在引入抽象層后,系統(tǒng)將具有很好的靈活性,在程序中盡量使用抽象層進(jìn)行編程,而將具體類寫在配置文件中,這樣一來,如果系統(tǒng)行為發(fā)生變化,只需要對抽象層進(jìn)行擴(kuò)展,并修改配置文件,而無須修改原有系統(tǒng)的源代碼,在不修改的情況下來擴(kuò)展系統(tǒng)的功能,滿足開閉原則的要求。
優(yōu)點(diǎn):通過抽象來搭建框架,建立類和類的關(guān)聯(lián),以減少類間的耦合性。而且以抽象搭建的系統(tǒng)要比以具體實(shí)現(xiàn)搭建的系統(tǒng)更加穩(wěn)定,擴(kuò)展性更高,同時(shí)也便于維護(hù)。
2.4 單一職責(zé)原則
一個類只負(fù)責(zé)一個功能領(lǐng)域中的相應(yīng)職責(zé),或者可以定義為:就一個類而言,應(yīng)該只有一個引起它變化的原因。
單一職責(zé)原則告訴我們:一個類不能太“累”!在軟件系統(tǒng)中,一個類(大到模塊,小到方法)承擔(dān)的職責(zé)越多,它被復(fù)用的可能性就越小,而且一個類承擔(dān)的職責(zé)過多,就相當(dāng)于將這些職責(zé)耦合在一起,當(dāng)其中一個職責(zé)變化時(shí),可能會影響其他職責(zé)的運(yùn)作,因此要將這些職責(zé)進(jìn)行分離,將不同的職責(zé)封裝在不同的類中,即將不同的變化原因封裝在不同的類中,如果多個職責(zé)總是同時(shí)發(fā)生改變則可將它們封裝在同一類中。
單一職責(zé)原則是實(shí)現(xiàn)高內(nèi)聚、低耦合的指導(dǎo)方針,它是最簡單但又最難運(yùn)用的原則,需要設(shè)計(jì)人員發(fā)現(xiàn)類的不同職責(zé)并將其分離,而發(fā)現(xiàn)類的多重職責(zé)需要設(shè)計(jì)人員具有較強(qiáng)的分析設(shè)計(jì)能力和相關(guān)實(shí)踐經(jīng)驗(yàn)。
優(yōu)點(diǎn):如果類與方法的職責(zé)劃分得很清晰,不但可以提高代碼的可讀性,更實(shí)際性地更降低了程序出錯的風(fēng)險(xiǎn),因?yàn)榍逦拇a會讓 bug 無處藏身,也有利于 bug 的追蹤,也就是降低了程序的維護(hù)成本。
2.5 迪米特法則(最少知道原則)
一個軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用
如果一個系統(tǒng)符合迪米特法則,那么當(dāng)其中某一個模塊發(fā)生修改時(shí),就會盡量少地影響其他模塊,擴(kuò)展會相對容易,這是對軟件實(shí)體之間通信的限制,迪米特法則要求限制軟件實(shí)體之間通信的寬度和深度。迪米特法則可降低系統(tǒng)的耦合度,使類與類之間保持松散的耦合關(guān)系。
迪米特法則要求我們在設(shè)計(jì)系統(tǒng)時(shí),應(yīng)該盡量減少對象之間的交互,如果兩個對象之間不必彼此直接通信,那么這兩個對象就不應(yīng)當(dāng)發(fā)生任何直接的相互作用,如果其中的一個對象需要調(diào)用另一個對象的某一個方法的話,可以通過第三者轉(zhuǎn)發(fā)這個調(diào)用。簡言之,就是通過引入一個合理的第三者來降低現(xiàn)有對象之間的耦合度。
在將迪米特法則運(yùn)用到系統(tǒng)設(shè)計(jì)中時(shí),要注意下面的幾點(diǎn):在類的劃分上,應(yīng)當(dāng)盡量創(chuàng)建松耦合的類,類之間的耦合度越低,就越有利于復(fù)用,一個處在松耦合中的類一旦被修改,不會對關(guān)聯(lián)的類造成太大波及。在類的結(jié)構(gòu)設(shè)計(jì)上,每一個類都應(yīng)當(dāng)盡量降低其成員變量和成員函數(shù)的訪問權(quán)限。在類的設(shè)計(jì)上,只要有可能,一個類型應(yīng)當(dāng)設(shè)計(jì)成不變類。在對其他類的引用上,一個對象對其他對象的引用應(yīng)當(dāng)降到最低。
優(yōu)點(diǎn):實(shí)踐迪米特法則可以良好地降低類與類之間的耦合,減少類與類之間的關(guān)聯(lián)程度,讓類與類之間的協(xié)作更加直接。
2.6 接口分離原則
使用多個專門的接口,而不使用單一的總接口,即客戶端不應(yīng)該依賴那些它不需要的接口。
根據(jù)接口隔離原則,當(dāng)一個接口太大時(shí),我們需要將它分割成一些更細(xì)小的接口,使用該接口的客戶端僅需知道與之相關(guān)的方法即可。每一個接口應(yīng)該承擔(dān)一種相對獨(dú)立的角色,不干不該干的事,該干的事都要干。
在使用接口隔離原則時(shí),我們需要注意控制接口的粒度,接口不能太小,如果太小會導(dǎo)致系統(tǒng)中接口泛濫,不利于維護(hù)。接口也不能太大,太大的接口將違背接口隔離原則,靈活性較差,使用起來很不方便。
優(yōu)點(diǎn):避免同一個接口里面包含不同類職責(zé)的方法,接口責(zé)任劃分更加明確,符合高內(nèi)聚低耦合的思想。
2.7 合成復(fù)用原則(六大之外的)
盡量使用對象組合,而不是繼承來達(dá)到復(fù)用的目的
合成復(fù)用原則就是在一個新的對象里通過關(guān)聯(lián)關(guān)系(包括組合關(guān)系和聚合關(guān)系)來使用一些已有的對象,使之成為新對象的一部分,新對象通過委派調(diào)用已有對象的方法達(dá)到復(fù)用功能的目的。簡而言之,復(fù)用時(shí)要盡量使用組合/聚合關(guān)系(關(guān)聯(lián)關(guān)系),少用繼承。
在面向?qū)ο笤O(shè)計(jì)中,可以通過兩種方法在不同的環(huán)境中復(fù)用已有的設(shè)計(jì)和實(shí)現(xiàn),即通過組合/聚合關(guān)系或通過繼承,但首先應(yīng)該考慮使用組合/聚合,組合/聚合可以使系統(tǒng)更加靈活,降低類與類之間的耦合度。一個類的變化對其他類造成的影響相對較少,其次才考慮繼承,在使用繼承時(shí),需要嚴(yán)格遵循里氏代換原則,有效使用繼承會有助于對問題的理解,降低復(fù)雜度,而濫用繼承反而會增加系統(tǒng)構(gòu)建和維護(hù)的難度以及系統(tǒng)的復(fù)雜度,因此需要慎重使用繼承復(fù)用。
優(yōu)點(diǎn):避免復(fù)用時(shí)濫用繼承,合理使用組合關(guān)系,增加靈活性。
2.8 六大原則 - 學(xué)習(xí)心得
六大原則中,開閉原則、里氏替換原則、依賴倒置原則聯(lián)系比較緊密,后兩者是實(shí)現(xiàn)開閉原則重要前提,使用中通過抽象化設(shè)計(jì)具有很好的可拓展性和可維護(hù)性。
知道最少原則可以降低耦合,減少不必要的交互,主張?jiān)O(shè)計(jì)接口和類要簡單易使用,將復(fù)雜的邏輯封裝并提供簡單易用的接口。
單一職責(zé)原則使項(xiàng)目中的類和方法根據(jù)職責(zé)細(xì)分,避免單個類負(fù)擔(dān)過重。職責(zé)越多,被復(fù)用的可能性就越小或使用起來越麻煩。
接口分離原則將功能復(fù)雜的接口細(xì)分成多個特定功能的接口,只做該做的事情,降低耦合,但是細(xì)化粒度不能太細(xì),容易導(dǎo)致接口過多。單一職責(zé)原則強(qiáng)調(diào)單個類內(nèi)部根據(jù)職責(zé)細(xì)分的設(shè)計(jì),接口分離原則強(qiáng)調(diào)類之間的耦合,盡量建立最小的依賴關(guān)系。
三、模式分類
《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書中設(shè)計(jì)模式有23個,它們各具特色,每個模式都為某一個可重復(fù)的設(shè)計(jì)問題提供了一套解決方案。根據(jù)它們的用途,設(shè)計(jì)模式可分為創(chuàng)建型(Creational),結(jié)構(gòu)型(Structural)和行為型(Behavioral)三種,其中創(chuàng)建型模式主要用于描述如何創(chuàng)建對象,結(jié)構(gòu)型模式主要用于描述如何實(shí)現(xiàn)類或?qū)ο蟮慕M合,行為型模式主要用于描述類或?qū)ο笤鯓咏换ヒ约霸鯓臃峙渎氊?zé)。
此外,根據(jù)某個模式主要是用于處理類之間的關(guān)系還是對象之間的關(guān)系,設(shè)計(jì)模式還可以分為類模式和對象模式。我們經(jīng)常將兩種分類方式結(jié)合使用,如單例模式是對象創(chuàng)建型模式,模板方法模式是類行為型模式。
3.1 創(chuàng)建型
創(chuàng)建型模式(Creational Pattern)對類的實(shí)例化過程進(jìn)行了抽象,能夠?qū)⒛K中對象的創(chuàng)建和對象的使用分離。為了使結(jié)構(gòu)更加清晰,外界對于這些對象只需要知道它們共同的接口,而不清楚其具體的實(shí)現(xiàn)細(xì)節(jié),使整個系統(tǒng)的設(shè)計(jì)更加符合單一職責(zé)原則。
簡單工廠模式(Simple Factory Pattern)
工廠方法模式(Factory Method Pattern)
抽象工廠模式(Abstract Factory Pattern)
單例模式(Singleton Pattern)
生成器模式(Builder Pattern)
原型模式(Prototype Pattern)
3.2 結(jié)構(gòu)型
結(jié)構(gòu)型模式(Structural Pattern)描述如何將類或者對 象結(jié)合在一起形成更大的結(jié)構(gòu),就像搭積木,可以通過 簡單積木的組合形成復(fù)雜的、功能更為強(qiáng)大的結(jié)構(gòu)。結(jié)構(gòu)型模式可以分為類結(jié)構(gòu)型模式和對象結(jié)構(gòu)型模式:
類結(jié)構(gòu)型模式關(guān)心類的組合,由多個類可以組合成一個更大的系統(tǒng),在類結(jié)構(gòu)型模式中一般只存在繼承關(guān)系和實(shí)現(xiàn)關(guān)系。
對象結(jié)構(gòu)型模式關(guān)心類與對象的組合,通過關(guān)聯(lián)關(guān)系使得在一 個類中定義另一個類的實(shí)例對象,然后通過該對象調(diào)用其方法。 根據(jù)“合成復(fù)用原則”,在系統(tǒng)中盡量使用關(guān)聯(lián)關(guān)系來替代繼 承關(guān)系,因此大部分結(jié)構(gòu)型模式都是對象結(jié)構(gòu)型模式。
外觀模式
適配器模式
橋接模式
代理模式
裝飾者模式
享元模式
3.3 行為型
行為型模式(Behavioral Pattern)是對在不同的對象之間劃分責(zé)任和算法的抽象化。行為型模式不僅僅關(guān)注類和對象的結(jié)構(gòu),而且重點(diǎn)關(guān)注它們之間的相互作用。通過行為型模式,可以更加清晰地劃分類與對象的職責(zé),并研究系統(tǒng)在運(yùn)行時(shí)實(shí)例對象之間的交互。
職責(zé)鏈模式
命令模式
解釋器模式
迭代器模式
中介者模式
備忘錄模式
觀察者模式
狀態(tài)模式
策略模式
模板方法模式
訪問者模式
四、創(chuàng)建型 - 設(shè)計(jì)模式
4.1 簡單工廠模式
簡單工廠模式(Simple Factory Pattern):專門定義一個類(工廠類)來負(fù)責(zé)創(chuàng)建其他類的實(shí)例??梢愿鶕?jù)創(chuàng)建方法的參數(shù)來返回不同類的實(shí)例,被創(chuàng)建的實(shí)例通常都具有共同的父類。
舉例:
簡單工廠模式像一個代工廠,一個工廠可以生產(chǎn)多種產(chǎn)品。舉個例子,一個飲料加工廠同時(shí)幫百事可樂和可口可樂生產(chǎn),加工廠根據(jù)輸入?yún)?shù)Type來生產(chǎn)不同的產(chǎn)品。
優(yōu)點(diǎn):
使用者只需要給工廠類傳入一個正確的約定好的參數(shù),就可以獲取你所需要的對象,而不需要知道其創(chuàng)建細(xì)節(jié),一定程度上減少系統(tǒng)的耦合。
客戶端無須知道所創(chuàng)建的具體產(chǎn)品類的類名,只需要知道具體產(chǎn)品類所對應(yīng)的參數(shù)即可,減少開發(fā)者的記憶成本。
缺點(diǎn):
如果業(yè)務(wù)上添加新產(chǎn)品的話,就需要修改工廠類原有的判斷邏輯,這其實(shí)是違背了開閉原則的。
在產(chǎn)品類型較多時(shí),有可能造成工廠邏輯過于復(fù)雜。所以簡單工廠模式比較適合產(chǎn)品種類比較少而且增多的概率很低的情況。
4.2 工廠方法模式
工廠方法模式(Factory Method Pattern)又稱為工廠模式,工廠父類負(fù)責(zé)定義創(chuàng)建產(chǎn)品對象的公共接口,而工廠子類則負(fù)責(zé)生成具體的產(chǎn)品對象,即通過不同的工廠子類來創(chuàng)建不同的產(chǎn)品對象。
舉例:
工廠方法和簡單工廠有一些區(qū)別,簡單工廠是由一個代工廠生產(chǎn)不同的產(chǎn)品,而工廠方法是對工廠進(jìn)行抽象化,不同產(chǎn)品都由專門的具體工廠來生產(chǎn)??煽诳蓸饭S專門生產(chǎn)可口可樂,百事可樂工廠專門生產(chǎn)百事可樂。
優(yōu)點(diǎn):
用戶只需要關(guān)心其所需產(chǎn)品對應(yīng)的具體工廠是哪一個即可,不需要關(guān)心產(chǎn)品的創(chuàng)建細(xì)節(jié),也不需要知道具體產(chǎn)品類的類名。
當(dāng)系統(tǒng)中加入新產(chǎn)品時(shí),不需要修改抽象工廠和抽象產(chǎn)品提供的接口,也無須修改客戶端和其他的具體工廠和具體產(chǎn)品,而只要添加一個具體工廠和與其對應(yīng)的具體產(chǎn)品就可以了,符合了開閉原則。
缺點(diǎn):
當(dāng)系統(tǒng)中加入新產(chǎn)品時(shí),除了需要提供新的產(chǎn)品類之外,還要提供與其對應(yīng)的具體工廠類。因此系統(tǒng)中類的個數(shù)將成對增加,增加了系統(tǒng)的復(fù)雜度。
4.3 抽象工廠模式
抽象工廠模式(Abstract Factory Pattern):提供一個創(chuàng)建一系列相關(guān)或相互依賴對象的接口,而無須指定它們具體的類。
舉例:
抽象工廠和工廠方法不同的地方在于,生產(chǎn)產(chǎn)品的工廠是抽象的。舉例,可口可樂公司生產(chǎn)可樂的同時(shí),也需要生產(chǎn)裝可樂的瓶子和箱子,瓶子和箱子也是可口可樂專屬定制的,同樣百事可樂公司也會有這個需求。這個時(shí)候我們的工廠不僅僅是生產(chǎn)可樂飲料的工廠,還必須同時(shí)生產(chǎn)同一主題的瓶子和箱子,所以它是一個抽象的主題工廠,專門生產(chǎn)同一主題的不同商品。
優(yōu)點(diǎn):
具體產(chǎn)品在應(yīng)用層代碼隔離,不需要關(guān)心產(chǎn)品細(xì)節(jié)。只需要知道自己需要的產(chǎn)品是屬于哪個工廠的即可 當(dāng)一個產(chǎn)品族中的多個對象被設(shè)計(jì)成一起工作時(shí),它能夠保證客戶端始終只使用同一個產(chǎn)品族中的對象。這對一些需要根據(jù)當(dāng)前環(huán)境來決定其行為的軟件系統(tǒng)來說,是一種非常實(shí)用的設(shè)計(jì)模式。
缺點(diǎn):
規(guī)定了所有可能被創(chuàng)建的產(chǎn)品集合,產(chǎn)品族中擴(kuò)展新的產(chǎn)品困難,需要修改抽象工廠的接口。
4.4 單例模式
單例模式(Singleton Pattern):單例模式確保某一個類只有一個實(shí)例,并提供一個訪問它的全劇訪問點(diǎn)。
舉例:
單例模式下,對應(yīng)類只能生成一個實(shí)例。就像一個王國只能有一個國王,一旦王國里的事務(wù)多起來,這唯一的國王也容易職責(zé)過重。
優(yōu)點(diǎn):
提供了對唯一實(shí)例的受控訪問。因?yàn)閱卫惙庋b了它的唯一實(shí)例,所以它可以嚴(yán)格控制客戶怎樣以及何時(shí)訪問它。
因?yàn)樵擃愒谙到y(tǒng)內(nèi)存中只存在一個對象,所以可以節(jié)約系統(tǒng)資源。
缺點(diǎn):
由于單例模式中沒有抽象層,因此單例類很難進(jìn)行擴(kuò)展。
對于有垃圾回收系統(tǒng)的語言 Java,C# 來說,如果對象長時(shí)間不被利用,則可能會被回收。那么如果這個單例持有一些數(shù)據(jù)的話,在回收后重新實(shí)例化時(shí)就不復(fù)存在了。
4.5 生成器模式
生成器模式(Builder Pattern):也叫創(chuàng)建者模式,它將一個復(fù)雜對象的構(gòu)建與它的表示分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示。
舉例:
生成器模式將復(fù)雜的創(chuàng)建邏輯進(jìn)行分割,例如生產(chǎn)汽車,分步驟創(chuàng)建安裝不同的零件。如果創(chuàng)建邏輯簡單則沒有拆分的必要。
優(yōu)點(diǎn):
客戶端不必知道產(chǎn)品內(nèi)部組成的細(xì)節(jié),將產(chǎn)品本身與產(chǎn)品的創(chuàng)建過程解耦,使得相同的創(chuàng)建過程可以創(chuàng)建不同的產(chǎn)品對象。
每一個具體建造者都相對獨(dú)立,而與其他的具體建造者無關(guān),因此可以很方便地替換具體建造者或增加新的具體建造者, 用戶使用不同的具體建造者即可得到不同的產(chǎn)品對象 。
增加新的具體建造者無須修改原有類庫的代碼,指揮者類針對抽象建造者類編程,系統(tǒng)擴(kuò)展方便,符合“開閉原則”。
可以更加精細(xì)地控制產(chǎn)品的創(chuàng)建過程 。將復(fù)雜產(chǎn)品的創(chuàng)建步驟分解在不同的方法中,使得創(chuàng)建過程更加清晰,也更方便使用程序來控制創(chuàng)建過程。
缺點(diǎn):
建造者模式所創(chuàng)建的產(chǎn)品一般具有較多的共同點(diǎn),其組成部分相似,如果產(chǎn)品之間的差異性很大,則不適合使用建造者模式,因此其使用范圍受到一定的限制。
如果產(chǎn)品的內(nèi)部變化復(fù)雜,可能會導(dǎo)致需要定義很多具體建造者類來實(shí)現(xiàn)這種變化,導(dǎo)致系統(tǒng)變得很龐大。
4.6 原型模式
原型模式(Prototype Pattern): 使用原型實(shí)例指定待創(chuàng)建對象的類型,并且通過復(fù)制這個原型來創(chuàng)建新的對象。
舉例:
原型模式就像復(fù)印技術(shù),根據(jù)原對象復(fù)印出一個新對象,并根據(jù)需求對新對象進(jìn)行微調(diào)。
優(yōu)點(diǎn):
可以利用原型模式簡化對象的創(chuàng)建過程,尤其是對一些創(chuàng)建過程繁瑣,包含對象層級比較多的對象來說,使用原型模式可以節(jié)約系統(tǒng)資源,提高對象生成的效率。
可以很方便得通過改變值來生成新的對象:有些對象之間的差別可能只在于某些值的不同;用原型模式可以快速復(fù)制出新的對象并手動修改值即可。
缺點(diǎn):
對象包含的所有對象都需要配備一個克隆的方法,這就使得在對象層級比較多的情況下,代碼量會很大,也更加復(fù)雜。
五、結(jié)構(gòu)型 - 設(shè)計(jì)模式
5.1 裝飾模式
裝飾模式(Decorator Pattern) :不改變原有對象的前提下,動態(tài)地給一個對象增加一些額外的功能。
舉例:
裝飾模式貼合開閉原則,在不改變原有類的情況下,對父類進(jìn)行改造或新增功能。舉例,定一個抽象類Tea,只能提供白開水,但是通過裝飾類BlackTea裝飾之后拓展了新功能,通過BlackTea類可以用白開水泡紅茶,還可以選擇加檸檬。
優(yōu)點(diǎn):
比繼承更加靈活:不同于在編譯期起作用的繼承;裝飾者模式可以在運(yùn)行時(shí)擴(kuò)展一個對象的功能。另外也可以通過配置文件在運(yùn)行時(shí)選擇不同的裝飾器,從而實(shí)現(xiàn)不同的行為。也可以通過不同的組合,可以實(shí)現(xiàn)不同效果。
符合“開閉原則”:裝飾者和被裝飾者可以獨(dú)立變化。用戶可以根據(jù)需要增加新的裝飾類,在使用時(shí)再對其進(jìn)行組合,原有代碼無須改變。
缺點(diǎn):
裝飾者模式需要創(chuàng)建一些具體裝飾類,會增加系統(tǒng)的復(fù)雜度。
5.2 外觀模式
外觀模式(Facade Pattern):外觀模式定義了一個高層接口,為子系統(tǒng)中的一組接口提供一個統(tǒng)一的接口。外觀模式又稱為門面模式,它是一種結(jié)構(gòu)型設(shè)計(jì)模式模式。
舉例:
外觀模式提供了簡單明確的接口,但是在內(nèi)部眾多子系統(tǒng)功能進(jìn)行整合。就像圖片緩存,內(nèi)部包含了涉及到其他子系統(tǒng)的如緩存、下載等處理,外觀模式將這些復(fù)雜的邏輯都隱藏了。在UIImageView和UIButton調(diào)用的時(shí)候,你只需要調(diào)一個setImageWithUrl:(NSString *)url接口就可以了,達(dá)到解耦合的目的。
優(yōu)點(diǎn):
實(shí)現(xiàn)了客戶端與子系統(tǒng)間的解耦:客戶端無需知道子系統(tǒng)的接口,簡化了客戶端調(diào)用子系統(tǒng)的調(diào)用過程,使得子系統(tǒng)使用起來更加容易。同時(shí)便于子系統(tǒng)的擴(kuò)展和維護(hù)。
符合迪米特法則(最少知道原則):子系統(tǒng)只需要將需要外部調(diào)用的接口暴露給外觀類即可,而且他的接口則可以隱藏起來。
缺點(diǎn):
違背了開閉原則:在不引入抽象外觀類的情況下,增加新的子系統(tǒng)可能需要修改外觀類或客戶端的代碼。
5.3 代理模式
代理模式(Proxy Pattern) :為某個對象提供一個代理,并由這個代理對象控制對原對象的訪問。
舉例:
代理模式像一個房屋中介,買家只能通過中介來買房,代理具備被代理類的所有功能,就像房東有賣房功能,中介也具有賣房功能。此外代理實(shí)例還可以幫助被代理實(shí)例進(jìn)行一些額外處理,比如中介可以幫助房東篩選優(yōu)質(zhì)買家的功能,幫助房東pass掉一些不符合條件的買家。還有消息隊(duì)列也是該模式。
優(yōu)點(diǎn):
降低系統(tǒng)的耦合度:代理模式能夠協(xié)調(diào)調(diào)用者和被調(diào)用者,在一定程度上降低了系 統(tǒng)的耦合度。
不同類型的代理可以對客戶端對目標(biāo)對象的訪問進(jìn)行不同的控制:
遠(yuǎn)程代理,使得客戶端可以訪問在遠(yuǎn)程機(jī)器上的對象,遠(yuǎn)程機(jī)器 可能具有更好的計(jì)算性能與處理速度,可以快速響應(yīng)并處理客戶端請求。
虛擬代理通過使用一個小對象來代表一個大對象,可以減少系統(tǒng)資源的消耗,對系統(tǒng)進(jìn)行優(yōu)化并提高運(yùn)行速度。
保護(hù)代理可以控制客戶端對真實(shí)對象的使用權(quán)限。
缺點(diǎn):
由于在客戶端和被代理對象之間增加了代理對象,因此可能會讓客戶端請求的速度變慢。
5.4 享元模式
享元模式(Flyweight Pattern):運(yùn)用共享技術(shù)復(fù)用大量細(xì)粒度的對象,降低程序內(nèi)存的占用,提高程序的性能。
舉例:
例如 UITableViewCell 的緩存機(jī)制,達(dá)到降低內(nèi)存消耗的目的。舉例,音樂服務(wù)根據(jù)收費(fèi)劃分出免費(fèi)用戶和會員用戶,免費(fèi)用戶只能聽部分免費(fèi)音樂,會員用戶可以聽全部的音樂,并且可以下載。雖然權(quán)限上二者間有一些區(qū)別,但是他們所享受的音樂來是自于同一個音樂庫,這樣所有的音樂都只需要保存一份就可以了。另外如果出現(xiàn)音樂庫里沒有的音樂時(shí),則需要新增該音樂,然后其他服務(wù)也可以享受新增的音樂,相當(dāng)于享元池或緩存池的功能。
享元模式區(qū)保證共享內(nèi)部狀態(tài)如音樂庫,而外部狀態(tài)根據(jù)不同需求定制如各種訪問權(quán)限,使用中不能去改變內(nèi)部狀態(tài),以達(dá)到共享的目的。
優(yōu)點(diǎn):
使用享元??梢詼p少內(nèi)存中對象的數(shù)量,使得相同對象或相似對象在內(nèi)存中只保存一份,降低系統(tǒng)的使用內(nèi)存,也可以提性能。
享元模式的外部狀態(tài)相對獨(dú)立,而且不會影響其內(nèi)部狀態(tài),從而使得享元對象可以在不同的環(huán)境中被共享。
缺點(diǎn):
使用享元模式需要分離出內(nèi)部狀態(tài)和外部狀態(tài),這使得程序的邏輯復(fù)雜化。
對象在緩沖池中的復(fù)用需要考慮線程問題。
5.5 橋接模式
橋接模式(Simple Factory Pattern):將抽象部分與它的實(shí)現(xiàn)部分分離,使它們都可以獨(dú)立地變化。
舉例:
盡管手機(jī)都有各自的不同之處,但是他們都有一個手機(jī)卡卡槽,卡槽里可以插不同運(yùn)營商的卡。不管手機(jī)和卡內(nèi)部如何改變,只要卡槽的行業(yè)標(biāo)準(zhǔn)沒有變,就都可以正常使用。橋接模式在于將復(fù)雜的類進(jìn)行分割,優(yōu)先對象組合的方式,就像將手機(jī)里的手機(jī)卡抽離出去新建一個類,實(shí)現(xiàn)手機(jī)實(shí)例持有一個手機(jī)卡實(shí)例的組合方式。而不是通過繼承來新建多個不同手機(jī)卡的手機(jī)子類。
SIMCardProtocol?協(xié)議相當(dāng)于行業(yè)標(biāo)準(zhǔn),所以手機(jī)卡都要遵循該協(xié)議。而各個手機(jī)生產(chǎn)商知道該協(xié)議,就可以直接利用該協(xié)議獲得 SIM 卡內(nèi)部信息。
優(yōu)點(diǎn):
擴(kuò)展性好,符合開閉原則:將抽象與實(shí)現(xiàn)分離,讓二者可以獨(dú)立變化
缺點(diǎn):
在設(shè)計(jì)之前,需要識別出兩個獨(dú)立變化的維度。
5.6 適配器模式
適配器模式(Adapter Pattern) :將一個接口轉(zhuǎn)換成客戶希望的另一個接口,使得原本由于接口不兼容而不能一起工作的那些類可以一起工作。適配器模式的別名是包裝器模式(Wrapper),是一種結(jié)構(gòu)型設(shè)計(jì)模式。
舉例:
適配器模式顧名思義,比如內(nèi)地用像港版插頭需要一個轉(zhuǎn)接頭。再比如iPhone的手機(jī)卡是特別小的 Nano 卡,把 Nano 卡拿到其他手機(jī)上不能貼合卡槽尺寸,所以我們需要加一個符合卡槽尺寸的卡套。
優(yōu)點(diǎn):
符合開閉原則:使用適配器而不需要改變現(xiàn)有類,提高類的復(fù)用性。
目標(biāo)類和適配器類解耦,提高程序擴(kuò)展性。
缺點(diǎn):
增加了系統(tǒng)的復(fù)雜性
六、行為型 - 設(shè)計(jì)模式
6.1 職責(zé)鏈模式
職責(zé)鏈模式(Chain of Responsibility Pattern):避免請求發(fā)送者與接收者耦合在一起,讓多個對象都有可能接收請求,將這些對象連接成一條鏈,并且沿著這條鏈傳遞請求,直到有對象處理它為止。職責(zé)鏈模式是一種對象行為型模式。
舉例:
職責(zé)鏈模式在 iOS 中有大量的應(yīng)用,比如事件響應(yīng)鏈,事件傳遞下來會先判斷該事件是不是應(yīng)該由自己處理,如果不是由自己處理則傳給下一位響應(yīng)者去處理,如此循環(huán)下去。需要注意的是要避免響應(yīng)鏈循環(huán)調(diào)用造成死循環(huán),還有當(dāng)所有的響應(yīng)者都無法處理時(shí)的情況。
優(yōu)點(diǎn):
職責(zé)鏈模式使得一個對象無須知道是其他哪一個對象處理其請求,對象僅需知道該請求會被處理即可,接收者和發(fā)送者都沒有對方的明確信息,且鏈中的對象不需要知道鏈的結(jié)構(gòu),由客戶端負(fù)責(zé)鏈的創(chuàng)建,降低了系統(tǒng)的耦合度。
請求處理對象僅需維持一個指向其后繼者的引用,而不需要維持它對所有的候選處理者的引用,可簡化對象的相互連接。
在給對象分派職責(zé)時(shí),職責(zé)鏈可以給我們更多的靈活性,可以通過在運(yùn)行時(shí)對該鏈進(jìn)行動態(tài)的增加或修改來增加或改變處理一個請求的職責(zé)。
在系統(tǒng)中增加一個新的具體請求處理者時(shí)無須修改原有系統(tǒng)的代碼,只需要在客戶端重新建鏈即可,從這一點(diǎn)來看是符合“開閉原則”的。
缺點(diǎn):
由于一個請求沒有明確的接收者,那么就不能保證它一定會被處理,該請求可能一直到鏈的末端都得不到處理;一個請求也可能因職責(zé)鏈沒有被正確配置而得不到處理。
對于比較長的職責(zé)鏈,請求的處理可能涉及到多個處理對象,系統(tǒng)性能將受到一定影響,而且在進(jìn)行代碼調(diào)試時(shí)不太方便。
如果建鏈不當(dāng),可能會造成循環(huán)調(diào)用,將導(dǎo)致系統(tǒng)陷入死循環(huán)。
6.2 命令模式
命令模式(Command Pattern):將一個請求封裝為一個對象,從而讓我們可用不同的請求對客戶進(jìn)行參數(shù)化;對請求排隊(duì)或者記錄請求日志,以及支持可撤銷的操作。命令模式是一種對象行為型模式,其別名為動作(Action)模式或事務(wù)(Transaction)模式。
舉例:
和之前代理模式中的舉例有些相似,不過命令模式的本質(zhì)是對命令進(jìn)行封裝,將發(fā)出命令的責(zé)任和執(zhí)行命令的責(zé)任分割開。例如遙控器是一個調(diào)用者,不同按鈕代表不同的命令,而電視是接收者。
優(yōu)點(diǎn):
降低系統(tǒng)的耦合度。由于請求者與接收者之間不存在直接引用,因此請求者與接收者之間實(shí)現(xiàn)完全解耦,相同的請求者可以對應(yīng)不同的接收者,同樣,相同的接收者也可以供不同的請求者使用,兩者之間具有良好的獨(dú)立性。
新的命令可以很容易地加入到系統(tǒng)中。由于增加新的具體命令類不會影響到其他類,因此增加新的具體命令類很容易,無須修改原有系統(tǒng)源代碼,甚至客戶類代碼,滿足“開閉原則”的要求。
可以比較容易地設(shè)計(jì)一個命令隊(duì)列或宏命令(組合命令)。
為請求的撤銷(Undo)和恢復(fù)(Redo)操作提供了一種設(shè)計(jì)和實(shí)現(xiàn)方案。
缺點(diǎn):
使用命令模式可能會導(dǎo)致某些系統(tǒng)有過多的具體命令類。因?yàn)獒槍γ恳粋€對請求接收者的調(diào)用操作都需要設(shè)計(jì)一個具體命令類,因此在某些系統(tǒng)中可能需要提供大量的具體命令類,這將影響命令模式的使用。
6.3 解釋器模式
解釋器模式(Interpreter Pattern):定義一個語言的文法,并且建立一個解釋器來解釋該語言中的句子,這里的“語言”是指使用規(guī)定格式和語法的代碼。解釋器模式是一種類行為型模式。
舉例:
說到解釋器模式,我們的編譯器,在對代碼進(jìn)行編譯的時(shí)候也用到了該模式。我們可以直接來做一個簡單的解釋器,一個給機(jī)器人下發(fā)指令的解釋器。
命令參數(shù)
direction 移動方向'up' 'down' 'left' 'right'
action 移動方式'move' 'run'
distance 移動距離an integer
表達(dá)式終結(jié)符號';'
通過建立一個映射關(guān)系可以很快將指令轉(zhuǎn)換成行為,例如up run 5;表示向上跑5米,而left move 12;表示向左移動12米。
優(yōu)點(diǎn):
易于改變和擴(kuò)展文法。由于在解釋器模式中使用類來表示語言的文法規(guī)則,因此可以通過繼承等機(jī)制來改變或擴(kuò)展文法。
每一條文法規(guī)則都可以表示為一個類,因此可以方便地實(shí)現(xiàn)一個簡單的語言。
實(shí)現(xiàn)文法較為容易。在抽象語法樹中每一個表達(dá)式節(jié)點(diǎn)類的實(shí)現(xiàn)方式都是相似的,這些類的代碼編寫都不會特別復(fù)雜,還可以通過一些工具自動生成節(jié)點(diǎn)類代碼。
增加新的解釋表達(dá)式較為方便。如果用戶需要增加新的解釋表達(dá)式只需要對應(yīng)增加一個新的終結(jié)符表達(dá)式或非終結(jié)符表達(dá)式類,原有表達(dá)式類代碼無須修改,符合“開閉原則”。
缺點(diǎn):
對于復(fù)雜文法難以維護(hù)。在解釋器模式中,每一條規(guī)則至少需要定義一個類,因此如果一個語言包含太多文法規(guī)則,類的個數(shù)將會急劇增加,導(dǎo)致系統(tǒng)難以管理和維護(hù),此時(shí)可以考慮使用語法分析程序等方式來取代解釋器模式。
執(zhí)行效率較低。由于在解釋器模式中使用了大量的循環(huán)和遞歸調(diào)用,因此在解釋較為復(fù)雜的句子時(shí)其速度很慢,而且代碼的調(diào)試過程也比較麻煩。
6.4 迭代器模式
迭代器模式(Iterator Pattern):提供一種方法來訪問聚合對象,而不用暴露這個對象的內(nèi)部表示,其別名為游標(biāo)(Cursor)。迭代器模式是一種對象行為型模式。
舉例:
迭代器幫助請求方獲取數(shù)據(jù),避免直接操作數(shù)據(jù)聚合類,使數(shù)據(jù)聚合類專注存儲數(shù)據(jù)。具體應(yīng)用有分頁等功能,分頁功能的迭代器將專門負(fù)責(zé)操作分頁數(shù)據(jù),將操作邏輯和數(shù)據(jù)源分離。
優(yōu)點(diǎn):
它支持以不同的方式遍歷一個聚合對象,在同一個聚合對象上可以定義多種遍歷方式。在迭代器模式中只需要用一個不同的迭代器來替換原有迭代器即可改變遍歷算法,我們也可以自己定義迭代器的子類以支持新的遍歷方式。
迭代器簡化了聚合類。由于引入了迭代器,在原有的聚合對象中不需要再自行提供數(shù)據(jù)遍歷等方法,這樣可以簡化聚合類的設(shè)計(jì)。
在迭代器模式中,由于引入了抽象層,增加新的聚合類和迭代器類都很方便,無須修改原有代碼,滿足“開閉原則”的要求。
缺點(diǎn):
由于迭代器模式將存儲數(shù)據(jù)和遍歷數(shù)據(jù)的職責(zé)分離,增加新的聚合類需要對應(yīng)增加新的迭代器類,類的個數(shù)成對增加,這在一定程度上增加了系統(tǒng)的復(fù)雜性。
抽象迭代器的設(shè)計(jì)難度較大,需要充分考慮到系統(tǒng)將來的擴(kuò)展,例如JDK內(nèi)置迭代器Iterator就無法實(shí)現(xiàn)逆向遍歷,如果需要實(shí)現(xiàn)逆向遍歷,只能通過其子類ListIterator等來實(shí)現(xiàn),而ListIterator迭代器無法用于操作Set類型的聚合對象。在自定義迭代器時(shí),創(chuàng)建一個考慮全面的抽象迭代器并不是件很容易的事情。
6.5 中介者模式
中介者模式(Mediator Pattern):用一個中介對象(中介者)來封裝一系列的對象交互,中介者使各對象不需要顯式地相互引用,從而使其耦合松散,而且可以獨(dú)立地改變它們之間的交互。中介者模式又稱為調(diào)停者模式,它是一種對象行為型模式。
舉例:
中介者模式將一個網(wǎng)狀的系統(tǒng)結(jié)構(gòu)變成一個以中介者對象為中心的星形結(jié)構(gòu),在這個星型結(jié)構(gòu)中,使用中介者對象與其他對象的一對多關(guān)系來取代原有對象之間的多對多關(guān)系。所有成員通過中介者交互,方便拓展新的成員,例如下面的例子,新增一個聊天室成員只需要新建一個成員實(shí)例,然后再在聊天室中介者那注冊就可以加入聊天室了。
優(yōu)點(diǎn):
中介者模式簡化了對象之間的交互,它用中介者和同事的一對多交互代替了原來同事之間的多對多交互,一對多關(guān)系更容易理解、維護(hù)和擴(kuò)展,將原本難以理解的網(wǎng)狀結(jié)構(gòu)轉(zhuǎn)換成相對簡單的星型結(jié)構(gòu)。
中介者模式可將各同事對象解耦。中介者有利于各同事之間的松耦合,我們可以獨(dú)立的改變和復(fù)用每一個同事和中介者,增加新的中介者和新的同事類都比較方便,更好地符合“開閉原則”。
可以減少子類生成,中介者將原本分布于多個對象間的行為集中在一起,改變這些行為只需生成新的中介者子類即可,這使各個同事類可被重用,無須對同事類進(jìn)行擴(kuò)展。
缺點(diǎn):
在具體中介者類中包含了大量同事之間的交互細(xì)節(jié),可能會導(dǎo)致具體中介者類非常復(fù)雜,使得系統(tǒng)難以維護(hù)。
6.6 備忘錄模式
備忘錄模式(Memento Pattern):在不破壞封裝的前提下,捕獲一個對象的內(nèi)部狀態(tài),并在該對象之外保存這個狀態(tài),這樣可以在以后將對象恢復(fù)到原先保存的狀態(tài)。它是一種對象行為型模式,其別名為Token。
舉例:
備忘錄模式提供了一種狀態(tài)恢復(fù)的實(shí)現(xiàn)機(jī)制,使得用戶可以方便地回到一個特定的歷史步驟,當(dāng)新的狀態(tài)無效或者存在問題時(shí),可以使用暫時(shí)存儲起來的備忘錄將狀態(tài)復(fù)原,當(dāng)前很多軟件都提供了撤銷操作,其中就使用了備忘錄模式。
我們用一個簡單的游戲存檔來舉例,這也是備忘錄模式的一種應(yīng)用。
優(yōu)點(diǎn):
它提供了一種狀態(tài)恢復(fù)的實(shí)現(xiàn)機(jī)制,使得用戶可以方便地回到一個特定的歷史步驟,當(dāng)新的狀態(tài)無效或者存在問題時(shí),可以使用暫時(shí)存儲起來的備忘錄將狀態(tài)復(fù)原。
備忘錄實(shí)現(xiàn)了對信息的封裝,一個備忘錄對象是一種原發(fā)器對象狀態(tài)的表示,不會被其他代碼所改動。備忘錄保存了原發(fā)器的狀態(tài),采用列表、堆棧等集合來存儲備忘錄對象可以實(shí)現(xiàn)多次撤銷操作。
缺點(diǎn):
資源消耗過大,如果需要保存的原發(fā)器類的成員變量太多,就不可避免需要占用大量的存儲空間,每保存一次對象的狀態(tài)都需要消耗一定的系統(tǒng)資源。
6.7 觀察者模式
觀察者模式(Observer Pattern):定義對象之間的一種一對多依賴關(guān)系,使得每當(dāng)一個對象狀態(tài)發(fā)生改變時(shí),其相關(guān)依賴對象皆得到通知并被自動更新。觀察者模式的別名包括發(fā)布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監(jiān)聽器(Source/Listener)模式或從屬者(Dependents)模式。觀察者模式是一種對象行為型模式。
舉例:
觀察者模式是使用頻率最高的設(shè)計(jì)模式之一,它用于建立一種對象與對象之間的依賴關(guān)系,一個對象發(fā)生改變時(shí)將自動通知其他對象,其他對象將相應(yīng)作出反應(yīng)。
在 iOS 中,觀察者模式經(jīng)常使用到,下面我就用 KVO 實(shí)現(xiàn)了一個通過氣象臺觀察天氣變化的簡單例子。
優(yōu)點(diǎn):
觀察者模式可以實(shí)現(xiàn)表示層和數(shù)據(jù)邏輯層的分離,定義了穩(wěn)定的消息更新傳遞機(jī)制,并抽象了更新接口,使得可以有各種各樣不同的表示層充當(dāng)具體觀察者角色。
觀察者模式在觀察目標(biāo)和觀察者之間建立一個抽象的耦合。觀察目標(biāo)只需要維持一個抽象觀察者的集合,無須了解其具體觀察者。由于觀察目標(biāo)和觀察者沒有緊密地耦合在一起,因此它們可以屬于不同的抽象化層次。
觀察者模式支持廣播通信,觀察目標(biāo)會向所有已注冊的觀察者對象發(fā)送通知,簡化了一對多系統(tǒng)設(shè)計(jì)的難度。
觀察者模式滿足“開閉原則”的要求,增加新的具體觀察者無須修改原有系統(tǒng)代碼,在具體觀察者與觀察目標(biāo)之間不存在關(guān)聯(lián)關(guān)系的情況下,增加新的觀察目標(biāo)也很方便。
缺點(diǎn):
如果一個觀察目標(biāo)對象有很多直接和間接觀察者,將所有的觀察者都通知到會花費(fèi)很多時(shí)間。
如果在觀察者和觀察目標(biāo)之間存在循環(huán)依賴,觀察目標(biāo)會觸發(fā)它們之間進(jìn)行循環(huán)調(diào)用,可能導(dǎo)致系統(tǒng)崩潰。
觀察者模式?jīng)]有相應(yīng)的機(jī)制讓觀察者知道所觀察的目標(biāo)對象是怎么發(fā)生變化的,而僅僅只是知道觀察目標(biāo)發(fā)生了變化。
6.8 狀態(tài)模式
狀態(tài)模式(State Pattern):允許一個對象在其內(nèi)部狀態(tài)改變時(shí)改變它的行為,對象看起來似乎修改了它的類。其別名為狀態(tài)對象(Objects for States),狀態(tài)模式是一種對象行為型模式。
舉例:
狀態(tài)模式用于解決復(fù)雜對象的狀態(tài)轉(zhuǎn)換以及不同狀態(tài)下行為的封裝問題。當(dāng)系統(tǒng)中某個對象存在多個狀態(tài),這些狀態(tài)之間可以進(jìn)行轉(zhuǎn)換,所以對象在不同狀態(tài)下具有不同行為時(shí)可以使用狀態(tài)模式。狀態(tài)模式將一個對象的狀態(tài)從該對象中分離出來,封裝到專門的狀態(tài)類中,使得對象狀態(tài)可以靈活變化。
我們可以做一個簡單的例子,我設(shè)計(jì)了一個銀行賬戶系統(tǒng),根據(jù)存錢余額來自動設(shè)置賬戶的狀態(tài),銀行賬戶在不同狀態(tài)下,進(jìn)行存錢、取錢和借錢的行為。在不同狀態(tài)下,這些行為得到的回復(fù)也不一樣,比如說沒有余額時(shí)無法取錢,只能借錢。
優(yōu)點(diǎn):
封裝了狀態(tài)的轉(zhuǎn)換規(guī)則,在狀態(tài)模式中可以將狀態(tài)的轉(zhuǎn)換代碼封裝在環(huán)境類或者具體狀態(tài)類中,可以對狀態(tài)轉(zhuǎn)換代碼進(jìn)行集中管理,而不是分散在一個個業(yè)務(wù)方法中。
將所有與某個狀態(tài)有關(guān)的行為放到一個類中,只需要注入一個不同的狀態(tài)對象即可使環(huán)境對象擁有不同的行為。
允許狀態(tài)轉(zhuǎn)換邏輯與狀態(tài)對象合成一體,而不是提供一個巨大的條件語句塊,狀態(tài)模式可以讓我們避免使用龐大的條件語句來將業(yè)務(wù)方法和狀態(tài)轉(zhuǎn)換代碼交織在一起。
可以讓多個環(huán)境對象共享一個狀態(tài)對象,從而減少系統(tǒng)中對象的個數(shù)。
缺點(diǎn):
狀態(tài)模式的使用必然會增加系統(tǒng)中類和對象的個數(shù),導(dǎo)致系統(tǒng)運(yùn)行開銷增大。
狀態(tài)模式的結(jié)構(gòu)與實(shí)現(xiàn)都較為復(fù)雜,如果使用不當(dāng)將導(dǎo)致程序結(jié)構(gòu)和代碼的混亂,增加系統(tǒng)設(shè)計(jì)的難度。
狀態(tài)模式對“開閉原則”的支持并不太好,增加新的狀態(tài)類需要修改那些負(fù)責(zé)狀態(tài)轉(zhuǎn)換的源代碼,否則無法轉(zhuǎn)換到新增狀態(tài);而且修改某個狀態(tài)類的行為也需修改對應(yīng)類的源代碼。
6.9 策略模式
策略模式(Strategy Pattern):定義一系列算法類,將每一個算法封裝起來,并讓它們可以相互替換,策略模式讓算法獨(dú)立于使用它的客戶而變化,也稱為政策模式(Policy)。策略模式是一種對象行為型模式。
舉例:
使用策略模式時(shí),我們可以定義一些策略類,每一個策略類中封裝一種具體的算法。在這里,每一個封裝算法的類我們都可以稱之為一種策略,根據(jù)傳入不同的策略類,使環(huán)境類執(zhí)行不同策略類中的算法。
生活中也有很多類似的例子,就比如說商城的會員卡機(jī)制。我們?nèi)ド坛琴徫锟梢酝ㄟ^持有的會員卡打折,購買同一件商品時(shí),持有不同等級的會員卡,能得到不同力度的折扣。下面的例子中我列舉了青銅、白銀、黃金三種 Vip 會員卡,傳入不同的會員卡最終需要支付的金額也會有所不同。
優(yōu)點(diǎn):
策略模式提供了對“開閉原則”的完美支持,用戶可以在不修改原有系統(tǒng)的基礎(chǔ)上選擇算法或行為,也可以靈活地增加新的算法或行為。
策略模式提供了管理相關(guān)的算法族的辦法。策略類的等級結(jié)構(gòu)定義了一個算法或行為族,恰當(dāng)使用繼承可以把公共的代碼移到抽象策略類中,從而避免重復(fù)的代碼。
策略模式提供了一種可以替換繼承關(guān)系的辦法。如果不使用策略模式,那么使用算法的環(huán)境類就可能會有一些子類,每一個子類提供一種不同的算法。但是,這樣一來算法的使用就和算法本身混在一起,不符合“單一職責(zé)原則”,決定使用哪一種算法的邏輯和該算法本身混合在一起,從而不可能再獨(dú)立演化;而且使用繼承無法實(shí)現(xiàn)算法或行為在程序運(yùn)行時(shí)的動態(tài)切換。
使用策略模式可以避免多重條件選擇語句。多重條件選擇語句不易維護(hù),它把采取哪一種算法或行為的邏輯與算法或行為本身的實(shí)現(xiàn)邏輯混合在一起,將它們?nèi)坑簿幋a(Hard Coding)在一個龐大的多重條件選擇語句中,比直接繼承環(huán)境類的辦法還要原始和落后。
策略模式提供了一種算法的復(fù)用機(jī)制,由于將算法單獨(dú)提取出來封裝在策略類中,因此不同的環(huán)境類可以方便地復(fù)用這些策略類。
缺點(diǎn):
客戶端必須知道所有的策略類,并自行決定使用哪一個策略類。這就意味著客戶端必須理解這些算法的區(qū)別,以便適時(shí)選擇恰當(dāng)?shù)乃惴?。換言之,策略模式只適用于客戶端知道所有的算法或行為的情況。
策略模式將造成系統(tǒng)產(chǎn)生很多具體策略類,任何細(xì)小的變化都將導(dǎo)致系統(tǒng)要增加一個新的具體策略類。
無法同時(shí)在客戶端使用多個策略類,也就是說,在使用策略模式時(shí),客戶端每次只能使用一個策略類,不支持使用一個策略類完成部分功能后再使用另一個策略類來完成剩余功能的情況。
6.10 模板方法模式
模板方法模式:定義一個操作中算法的框架,而將一些步驟延遲到子類中。模板方法模式使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。
舉例:
模板方法模式在 iOS 中的應(yīng)用也非常多,如 UIViewController 的生命周期函數(shù),定義在父類,子類可以重寫這些函數(shù)。
模板方法模式具體應(yīng)用又分為三類:
抽象方法:一個抽象方法由抽象類聲明、由其具體子類實(shí)現(xiàn)。
具體方法:一個具體方法由一個抽象類或具體類聲明并實(shí)現(xiàn),其子類可以進(jìn)行覆蓋也可以直接繼承。
鉤子方法:一個鉤子方法由一個抽象類或具體類聲明并實(shí)現(xiàn),而其子類可能會加以擴(kuò)展。通常在父類中給出的實(shí)現(xiàn)是一個空實(shí)現(xiàn),并以該空實(shí)現(xiàn)作為方法的默認(rèn)實(shí)現(xiàn),當(dāng)然鉤子方法也可以提供一個非空的默認(rèn)實(shí)現(xiàn)。通過在子類中實(shí)現(xiàn)的鉤子方法對父類方法的執(zhí)行進(jìn)行約束,實(shí)現(xiàn)子類對父類行為的反向控制。
下面給出一個例子,在給定一個有固定模板的烹飪教程的情況下,根據(jù)不同烹飪需求對教程中的內(nèi)容進(jìn)行動態(tài)調(diào)整。
第一個步驟prepareIngredients, 父類中沒有具體實(shí)現(xiàn)為抽象方法,子類中直接覆蓋。
第二個步驟加食用油方法addFat被鉤子方法isHealthyFood?給跳過了。
第三步addIngredients?在父類中同樣是抽象方法,子類直接覆蓋。
第四步addFlavouring?在父類中有具體實(shí)現(xiàn),子類繼承父類的「加鹽操作」后又增加了新的「加黑胡椒」操作。
優(yōu)點(diǎn):
在父類中形式化地定義一個算法,而由它的子類來實(shí)現(xiàn)細(xì)節(jié)的處理,在子類實(shí)現(xiàn)詳細(xì)的處理算法時(shí)并不會改變算法中步驟的執(zhí)行次序。
模板方法模式是一種代碼復(fù)用技術(shù),它在類庫設(shè)計(jì)中尤為重要,它提取了類庫中的公共行為,將公共行為放在父類中,而通過其子類來實(shí)現(xiàn)不同的行為,它鼓勵我們恰當(dāng)使用繼承來實(shí)現(xiàn)代碼復(fù)用。
可實(shí)現(xiàn)一種反向控制結(jié)構(gòu),通過子類覆蓋父類的鉤子方法來決定某一特定步驟是否需要執(zhí)行。
在模板方法模式中可以通過子類來覆蓋父類的基本方法,不同的子類可以提供基本方法的不同實(shí)現(xiàn),更換和增加新的子類很方便,符合單一職責(zé)原則和開閉原則。
缺點(diǎn):
需要為每一個基本方法的不同實(shí)現(xiàn)提供一個子類,如果父類中可變的基本方法太多,將會導(dǎo)致類的個數(shù)增加,系統(tǒng)更加龐大,設(shè)計(jì)也更加抽象,此時(shí),可結(jié)合橋接模式來進(jìn)行設(shè)計(jì)。
6.11 訪問者模式
訪問者模式(Visitor Pattern):提供一個作用于某對象結(jié)構(gòu)中的各元素的操作表示,它使我們可以在不改變各元素的類的前提下定義作用于這些元素的新操作。訪問者模式是一種對象行為型模式。
舉例:
訪問者模式是一種較為復(fù)雜的行為型設(shè)計(jì)模式,它包含訪問者和被訪問元素兩個主要組成部分,這些被訪問的元素通常具有不同的類型,且不同的訪問者可以對它們進(jìn)行不同的訪問操作。訪問者模式使得用戶可以在不修改現(xiàn)有系統(tǒng)的情況下擴(kuò)展系統(tǒng)的功能,為這些不同類型的元素增加新的操作。
在使用訪問者模式時(shí),被訪問元素通常不是單獨(dú)存在的,它們存儲在一個集合中,這個集合被稱為「對象結(jié)構(gòu)」,訪問者通過遍歷對象結(jié)構(gòu)實(shí)現(xiàn)對其中存儲的元素的逐個操作。通過一個簡單的例子了解訪問者模式,訪問者有財(cái)務(wù)部門FADepartment和 HR 部門HRDepartment,通過訪問雇員Employee來查看雇員的工作情況。
優(yōu)點(diǎn):
增加新的訪問操作很方便。使用訪問者模式,增加新的訪問操作就意味著增加一個新的具體訪問者類,實(shí)現(xiàn)簡單,無須修改源代碼,符合“開閉原則”。
將有關(guān)元素對象的訪問行為集中到一個訪問者對象中,而不是分散在一個個的元素類中。類的職責(zé)更加清晰,有利于對象結(jié)構(gòu)中元素對象的復(fù)用,相同的對象結(jié)構(gòu)可以供多個不同的訪問者訪問。
讓用戶能夠在不修改現(xiàn)有元素類層次結(jié)構(gòu)的情況下,定義作用于該層次結(jié)構(gòu)的操作。
缺點(diǎn):
增加新的元素類很困難。在訪問者模式中,每增加一個新的元素類都意味著要在抽象訪問者角色中增加一個新的抽象操作,并在每一個具體訪問者類中增加相應(yīng)的具體操作,這違背了“開閉原則”的要求。
破壞封裝。訪問者模式要求訪問者對象訪問并調(diào)用每一個元素對象的操作,這意味著元素對象有時(shí)候必須暴露一些自己的內(nèi)部操作和內(nèi)部狀態(tài),否則無法供訪問者訪問。
總結(jié)
系統(tǒng)地學(xué)習(xí)設(shè)計(jì)模式后,你可以在過往的開發(fā)經(jīng)歷中發(fā)現(xiàn),設(shè)計(jì)模式是無處不在的。在學(xué)習(xí)設(shè)計(jì)模式之前的很多時(shí)候我們是憑借過往經(jīng)驗(yàn)和智慧來完善系統(tǒng)的設(shè)計(jì),而這些經(jīng)驗(yàn)很多和某個設(shè)計(jì)模式的思想不謀而合。
還有一些地方?jīng)]有完全理解,文中有誤之處還望不吝指出。
參考
https://juejin.im/user/57f8ffda2e958a005581e3c0/posts
https://design-patterns.readthedocs.io/zh_CN/latest/index.html
https://blog.csdn.net/lovelion/article/details/17517213
https://github.com/skyming/Trip-to-iOS-Design-Patterns