什么是設(shè)計(jì)模式
? 要了解設(shè)計(jì)模式祈搜,首先得清楚什么是模式胚股。什么是模式笼痛?模式即解決一類問題的方法論,簡(jiǎn)單得來說琅拌,就是將解決某類問題的方法歸納總結(jié)到理論高度缨伊,就形成了模式。
? 設(shè)計(jì)模式就是將代碼設(shè)計(jì)經(jīng)驗(yàn)歸納總結(jié)到理論高度而形成的财忽。其目的就在于:1)可重用代碼倘核,2)讓代碼更容易為他人理解,3)保證代碼的可靠性即彪。
? 使用面向?qū)ο蟮恼Z言很容易紧唱,但是做到面向?qū)ο髤s很難。更多人用的是面向?qū)ο蟮恼Z言寫出結(jié)構(gòu)化的代碼隶校,想想自己編寫的代碼有多少是不用修改源碼可以真正實(shí)現(xiàn)重用漏益,或者可以實(shí)現(xiàn)拿來主義。這是一件很正常的事深胳,我在學(xué)習(xí)過程當(dāng)中绰疤,老師們總是在說c到c++的面向?qū)ο笫且环N巨大的進(jìn)步,面向?qū)ο笠彩菢O為難以理解的存在舞终;而在開始的學(xué)習(xí)過程中轻庆,我發(fā)現(xiàn)c++和c好像差別也不大,不就是多了一個(gè)類和對(duì)象嗎敛劝?但隨著愈發(fā)深入的學(xué)習(xí)使我發(fā)現(xiàn)余爆,事實(shí)并不是那么簡(jiǎn)單,老師們舉例時(shí)總是喜歡用到簡(jiǎn)單的對(duì)象群體夸盟,比如:人蛾方,再到男人、女人上陕,再到擁有具體家庭身份的父親桩砰、母親、孩子释簿。用這些來說明類亚隅、對(duì)象、繼承......似乎都顯得面向?qū)ο笫且患p而易舉的事庶溶。
? 但事實(shí)真是如此嗎煮纵?封裝沉删、粒度、依賴關(guān)系醉途、靈活性、性能砖茸、演化隘擎、復(fù)用等等,當(dāng)這些在一個(gè)系統(tǒng)當(dāng)中交錯(cuò)相連凉夯,互相耦合货葬,甚至有些東西還互相沖突時(shí),你會(huì)發(fā)現(xiàn)自己可能連將系統(tǒng)對(duì)象化都是那么的困難劲够。
? 而在解決這些問題的過程當(dāng)中震桶,也就慢慢形成了一套被反復(fù)使用、為多數(shù)人知曉征绎、再由人分類編目的代碼設(shè)計(jì)經(jīng)驗(yàn)總結(jié)——設(shè)計(jì)模式蹲姐。
設(shè)計(jì)原則
? 模式既然作為一套解決方案,自然不可能是沒有規(guī)律而言的人柿,而其所遵循的內(nèi)在規(guī)律就是設(shè)計(jì)原則柴墩。在學(xué)習(xí)設(shè)計(jì)模式的過程當(dāng)中,不能脫離原則去看設(shè)計(jì)模式凫岖,而是應(yīng)該透過設(shè)計(jì)模式去理解設(shè)計(jì)原則江咳,只有深深地把握了設(shè)計(jì)原則,才能寫出真正的面向?qū)ο蟠a哥放,甚至創(chuàng)造自己的模式歼指。
-
開閉原則(Open Close Principle)
? 開閉原則的意思是:對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉甥雕。在程序需要進(jìn)行拓展的時(shí)候踩身,不要去修改原有的代碼。這樣是為了使程序的擴(kuò)展性更好犀农,更加易于維護(hù)和升級(jí)惰赋。而想要達(dá)到這樣的效果,就需要使用接口和抽象類呵哨。
-
里氏替換原則(Liskov Substitution Principle)
? 里氏替換原則中說赁濒,任何基類可以出現(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ī)范惯雳。
-
依賴倒置原則(Dependence Inversion Principle)
? 依賴倒置原則是開閉原則的基礎(chǔ),具體內(nèi)容:抽象不應(yīng)該依賴具體鸿摇,而是具體應(yīng)當(dāng)依賴抽象石景;高層模塊不應(yīng)該依賴底層模塊,而是高層和底層模塊都要依賴抽象拙吉。因?yàn)槌橄蟛攀欠€(wěn)定的潮孽,這個(gè)原則想要說明的就是針對(duì)接口編程。
-
接口分離原則(Interface Segregation Principle)
? 這個(gè)原則的意思是:使用多個(gè)隔離的接口筷黔,比使用單個(gè)接口要好往史。它還有另外一個(gè)意思是:降低類之間的耦合度。這個(gè)原則所要求的就是盡量將接口最小化佛舱,避免一個(gè)接口當(dāng)中擁有太多不相關(guān)的功能椎例。
-
迪米特法則,又稱最少知道原則(Demeter Principle)
? 最少知道原則是指:如果兩個(gè)軟件實(shí)體無須直接通信请祖,那么就不應(yīng)當(dāng)發(fā)生直接的相互調(diào)用粟矿,可以通過第三方轉(zhuǎn)發(fā)該調(diào)用。其目的是降低類之間的耦合度损拢,提高模塊的相對(duì)獨(dú)立性陌粹。迪米特法則在解決訪問耦合方面有著很大的作用,但是其本身的應(yīng)用也有著一個(gè)很大的缺點(diǎn)福压,就是對(duì)象之間的通信造成性能的損失掏秩,這是在使用過程中,需要去折衷考慮的荆姆。
-
組合復(fù)用原則(Composite Reuse Principle)
? 組合復(fù)用原則或者說組合優(yōu)先原則蒙幻,也就是在進(jìn)行功能復(fù)用的過程當(dāng)中,組合往往是比繼承更好的選擇胆筒。這是因?yàn)槔^承的形式會(huì)使得父類的實(shí)現(xiàn)細(xì)節(jié)對(duì)子類可見邮破,從而違背了封裝的目的。
-
單一職責(zé)原則(Single Responsibility Principle)
? 一個(gè)類只允許有一個(gè)職責(zé)仆救,即只有一個(gè)導(dǎo)致該類變更的原因抒和。類職責(zé)的變化往往就是導(dǎo)致類變化的原因:也就是說如果一個(gè)類具有多種職責(zé),就會(huì)有多種導(dǎo)致這個(gè)類變化的原因彤蔽,從而導(dǎo)致這個(gè)類的維護(hù)變得困難摧莽。
? 設(shè)計(jì)模式是設(shè)計(jì)原則在應(yīng)用體現(xiàn),設(shè)計(jì)原則是解決面向?qū)ο髥栴}處理方法顿痪。在面對(duì)訪問耦合的情況下镊辕,有針對(duì)接口編程油够、接口分離、迪米特法則征懈;處理繼承耦合問題石咬,有里氏替換原則、優(yōu)先組合原則卖哎;在保證類的內(nèi)聚時(shí)碌补,可以采用單一職責(zé)原則、集中類的信息與行為棉饶。這一系列的原則都是為了一個(gè)目的——盡可能的實(shí)現(xiàn)開閉。設(shè)計(jì)模式不是萬能的镇匀,它是設(shè)計(jì)原則互相取舍的成果照藻,而學(xué)習(xí)設(shè)計(jì)模式是如何抓住變化和穩(wěn)定的界線才是設(shè)計(jì)模式的真諦。
GOF-23 模式分類
? 從目的來看汗侵,即模式是用來完成什么工作的幸缕;可以劃分為創(chuàng)建型、結(jié)構(gòu)型和行為型晰韵。創(chuàng)建型模式與對(duì)象的創(chuàng)建有關(guān)发乔,結(jié)構(gòu)型模式處理類或?qū)ο蟮慕M合,行為型模式對(duì)類和對(duì)象怎樣分配職責(zé)進(jìn)行描述雪猪。
? 從范圍來看栏尚,即模式是作用于類還是對(duì)象;可以劃分為類模式和對(duì)象模式只恨。類模式處理類和子類之間的關(guān)系译仗,這些關(guān)系通過繼承建立,是靜態(tài)的官觅,在編譯時(shí)刻就確定下來了纵菌;對(duì)象模式處理對(duì)象間的關(guān)系,這些關(guān)系可以在運(yùn)行時(shí)刻變化休涤,更加具有動(dòng)態(tài)性咱圆。
組合之下,就產(chǎn)生了以下六種模式類別:
類創(chuàng)建型模式:將對(duì)象的創(chuàng)建工作延遲到子類中功氨。
對(duì)象創(chuàng)建型模式:將對(duì)象的創(chuàng)建延工作遲到另一個(gè)對(duì)象的中序苏。
類結(jié)構(gòu)型模式:使用繼承機(jī)制來組合類。
對(duì)象創(chuàng)建型模式:描述對(duì)象的組裝形式捷凄。
類行為型模式:使用繼承描述算法和控制流杠览。
對(duì)象行為型模式:描述了一組對(duì)象怎樣協(xié)作完成單個(gè)對(duì)象所無法完成的任務(wù)。
從封裝變化的角度來看
? GOF(“四人組”)對(duì)設(shè)計(jì)模式的分類更多的是從用途方法進(jìn)行劃分纵势,而現(xiàn)在踱阿,我們希望從設(shè)計(jì)模式中變化和穩(wěn)定結(jié)構(gòu)分隔上來理解所有的設(shè)計(jì)模式管钳,或許有著不同的收獲。
? 首先要明白的是软舌,獲得最大限度復(fù)用的關(guān)鍵在于對(duì)新需求和已有需求發(fā)生變化的預(yù)見性才漆,這也就要求系統(tǒng)設(shè)計(jì)能夠相應(yīng)地改進(jìn)。而設(shè)計(jì)模式可以確保系統(tǒng)以特定的方式變化佛点,從而避免系統(tǒng)的重新設(shè)計(jì)醇滥,并且設(shè)計(jì)模式同樣允許系統(tǒng)結(jié)構(gòu)的某個(gè)方面的變化獨(dú)立于其他方面,這樣就在一定程度上加強(qiáng)了系統(tǒng)的健壯性超营。
? 根據(jù)封裝變化鸳玩,可以將設(shè)計(jì)模式劃分為:組件協(xié)作、單一職責(zé)演闭、對(duì)象創(chuàng)建不跟、對(duì)象性能、接口隔離米碰、狀態(tài)變化窝革、數(shù)據(jù)結(jié)構(gòu)、行為變化以及領(lǐng)域問題等等吕座。
設(shè)計(jì)模式之組件協(xié)作
? 現(xiàn)代軟件專業(yè)分工之后的第一個(gè)結(jié)果就是“框架與應(yīng)用程序的劃分”虐译,“組件協(xié)作”就是通過晚期綁定,來實(shí)現(xiàn)框架與應(yīng)用程序之間的松耦合吴趴,是二者之間協(xié)作時(shí)常用的模式漆诽。其典型模式就是模板方法、策略模式和觀察者锣枝。
模板方法——類行為型模式
- 意圖
? 定義一個(gè)操作中的算法的骨架拴泌,并將其中一些步驟的實(shí)現(xiàn)延遲到子類中。模板方法使得子類可以重定義一個(gè)算法的步驟而不會(huì)改變算法的結(jié)構(gòu)惊橱。
- 實(shí)例
? 程序開發(fā)庫和應(yīng)用程序之間的調(diào)用蚪腐。假設(shè)現(xiàn)在存在一個(gè)開發(fā)庫惰蜜,其內(nèi)容是實(shí)現(xiàn)對(duì)一個(gè)文件或信息的操作谭羔,操作包含:open忽刽、read适掰、operation瑟幕、commit赡模、close创夜。但是呢溶耘!只有open觅廓、commit鼻忠、close是確定的,其中read需要根據(jù)具體的operation來確定讀取方式杈绸,所以這兩個(gè)方法是需要開發(fā)人員自己去實(shí)現(xiàn)的帖蔓。
? 那我們第一次的實(shí)現(xiàn)可能就是這種方式:
//標(biāo)準(zhǔn)庫實(shí)現(xiàn)
public class StdLibrary {
public void open(String s){
System.out.println("open: "+s);
}
public void commit(){
System.out.println("commit operation!");
}
public void close(String s){
System.out.println("close: "+s);
}
}
//應(yīng)用程序的實(shí)現(xiàn)
public class MyApplication {
public void read(String s,String type){
System.out.println("使用"+type+"方式read: "+s);
}
public void operation(){
System.out.println("operation");
}
}
//或者這樣實(shí)現(xiàn)
public class MyApplication extends StdLibrary{
public void read(String s,String type){
System.out.println("使用"+type+"方式read: "+s);
}
public void operation(){
System.out.println("operation");
}
}
//這里兩種實(shí)現(xiàn)方式的代碼調(diào)用寫在一起矮瘟,就不分開了。
public class MyClient {
public static void main(String[] args){
//方式1
String file = "ss.txt";
StdLibrary lib = new StdLibrary();
MyApplication app = new MyApplication();
lib.open(file);
app.read(file,"STD");
app.operation();
lib.commit();
lib.close(file);
//方式2
MyApplication app = new MyApplication();
app.open(file);
app.read(file,"STD");
app.operation();
app.commit();
app.close(file);
}
}
? 這種實(shí)現(xiàn)塑娇,無論是方式1還是方式2澈侠,對(duì)于僅僅是作為應(yīng)用來說,當(dāng)然是可以的埋酬。其問題主要在什么地方呢哨啃?就方式1 而言,他是必須要使用者了解開發(fā)庫和應(yīng)用程序兩個(gè)類写妥,才能夠正確的去應(yīng)用拳球。
? 方式2相較于方式1,使用更加的簡(jiǎn)單些珍特,但是仍然有不完善的地方祝峻,就是調(diào)用者,需要知道各個(gè)方法的執(zhí)行順序次坡,這也是1和2共同存在的問題。而這剛好就是Template Method發(fā)揮的時(shí)候了画畅,一系列操作有著明確的順序砸琅,并且有著部分的操作不變,剩下的操作待定轴踱。
//按照Template Method結(jié)構(gòu)可以將標(biāo)準(zhǔn)庫作出如下修改
public abstract class StdLibrary {
public void open(String s){
System.out.println("open: "+s);
}
public abstract void read(String s, String type);
public abstract void operation();
public void commit(){
System.out.println("commit operation!");
}
public void close(String s){
System.out.println("close: "+s);
}
public void doOperation(String s,String type){
open(s);
read(s,"STD");
operation();
commit();
close(s);
}
}
? 在修改過程中症脂,將原來的類修改成了抽象類,并且新增了兩個(gè)抽象方法和一個(gè)doOperation()
淫僻。通過使用抽象操作定義一個(gè)算法中的一些步驟诱篷,模板方法確定了它們的先后順序,但它允許Library和Application子類改變這些具體的步驟以滿足它們各自的需求雳灵,并且還對(duì)外隱藏了算法的實(shí)現(xiàn)棕所。當(dāng)然,如果標(biāo)準(zhǔn)庫中的不變方法不能被重定義悯辙,那么就應(yīng)該將其設(shè)置為private或者final琳省。
//修改過后的Appliaction和Client
public class MyApplication extends StdLibrary {
@Override
public void read(String s, String type){
System.out.println("使用"+type+"方式read: "+s);
}
@Override
public void operation(){
System.out.println("operation");
}
}
public class MyClient {
public static void main(String[] args){
String file = "ss.txt";
MyApplication app = new MyApplication();
app.doOperation(file,"STD");
}
}
? 模板方法的使用在類庫當(dāng)中極為常見,尤其是在c++的類庫當(dāng)中躲撰,它是一種基本的代碼復(fù)用技術(shù)针贬。這種實(shí)現(xiàn)方式,產(chǎn)生了一種反向的控制結(jié)構(gòu)拢蛋,或者我們稱之為“好萊塢法則”桦他,即“別找我們,我們找你”谆棱;換名話說快压,這種反向控制結(jié)構(gòu)就是父類調(diào)用了子類的操作(父類中的doOperation()
調(diào)用了子類實(shí)現(xiàn)的read()
和operation()
)圆仔,因?yàn)樵谄綍r(shí),我們的繼承代碼復(fù)用更多的是調(diào)用子類調(diào)用父類的操作嗓节。
-
結(jié)構(gòu)
templateMethod.png -
參與者
-
AbstractClass(StdLibrary)
定義抽象的原語操作(可變部分)荧缘。
實(shí)現(xiàn)一個(gè)模板方法(
templateMethod()
),定義算法的骨架拦宣。 -
ConcreteClass(具體的實(shí)現(xiàn)類截粗,如MyApplication)
實(shí)現(xiàn)原語操作以完成算法中與特定子類相關(guān)的步驟。
除了以上參與者之外鸵隧,還可以有OperatedObject這樣一個(gè)參與者即被操作對(duì)象绸罗。比如對(duì)文檔的操作,文檔又有不同的類型豆瘫,如pdf珊蟀、word、txt等等外驱;這種情況下育灸,就需要根據(jù)不同的文檔類型,定制不同的操作昵宇,即一個(gè)ConcreteClass對(duì)應(yīng)一個(gè)OperatedObject磅崭,相當(dāng)于對(duì)結(jié)構(gòu)當(dāng)中由一個(gè)特定操作對(duì)象,擴(kuò)展到多個(gè)操作對(duì)象瓦哎,并且每個(gè)操作對(duì)象對(duì)應(yīng)一個(gè)模板方法子類砸喻。
-
-
適用性
對(duì)于模板方法的特性,其可以應(yīng)用于下列情況:
- 一次性實(shí)現(xiàn)一個(gè)算法的不變部分蒋譬,并將可變的行為留給子類來實(shí)現(xiàn)割岛。
- 各子類中公共的行為應(yīng)被提取出來并集中到一個(gè)公共父類中,以避免代碼重復(fù)犯助。重構(gòu)方式即為首先識(shí)別現(xiàn)有代碼中的不同之處癣漆,并且將不同之處分離為新的操作。最后用一個(gè)模板方法調(diào)用這些新的操作剂买,來替換這些不同的代碼扑媚。
- 控制子類的擴(kuò)展。模板方法只有特定點(diǎn)調(diào)用"hook"操作雷恃,這樣就只允許在這些擴(kuò)展點(diǎn)進(jìn)行相應(yīng)的擴(kuò)展疆股。
-
相關(guān)模式
? Factory Method經(jīng)常被Template Method所調(diào)用。比如在參與者當(dāng)中提到的倒槐,如果需要操作不同的文件對(duì)象旬痹,那么在操作的過程中就需要
read()
方法返回不同的文件對(duì)象,而這個(gè)read()
方法不正是一個(gè)Factory Method。? Strategy:Template Method使用繼承來改變算法的一部分两残,而Strategy使用委托來改變整個(gè)算法永毅。
-
思考
- 訪問控制 在定義模板的時(shí)候,除了簡(jiǎn)單的定義原語操作和算法骨架之外人弓,操作的控制權(quán)也是需要考慮的沼死。原語操作是可以被重定義的,所以不能設(shè)置為final崔赌,還有原語操作能否為其他不相關(guān)的類所調(diào)用意蛀,如果不能則可以設(shè)置為protected或者default。模板方法一般是不讓子類重定義的健芭,因此就需要設(shè)置為final.
- 原語操作數(shù)量 定義模板方法的一個(gè)重要目的就是盡量減少一個(gè)子類具體實(shí)現(xiàn)該算法時(shí)县钥,必須重定義的那些原語操作的數(shù)目。因?yàn)榇嚷酰枰囟x的操作越多若贮,應(yīng)用程序就越冗長(zhǎng)。
- 命名約定 對(duì)于需要重定義的操作可以加上一個(gè)特定的前綴以便開發(fā)人員識(shí)別它們痒留。
- hook操作 hook操作就是指那些在模板方法中定義的可以重定義的操作谴麦,子類在必要的時(shí)候可以進(jìn)行擴(kuò)展。當(dāng)然伸头,如果可以使用父類的操作匾效,不擴(kuò)展也是可以的;因此熊锭,在Template Method中弧轧,應(yīng)該去指明哪些操作是不能被重定義的雪侥、哪些是hook(可以被重定義)以及哪些是抽象操作(必須被重定義)碗殷。
策略模式——對(duì)象行為型模式
-
意圖
? 定義一系列的算法,把它們一個(gè)個(gè)封裝起來速缨,并且使它們可相互替換锌妻。Strategy使得算法可以獨(dú)立于使用它的客戶而變化。
-
實(shí)例
? 策略模式是一種非常經(jīng)典的設(shè)計(jì)模式旬牲,可能也是大家經(jīng)常所見到和使用的設(shè)計(jì)模式仿粹;重構(gòu)過程中選擇使用策略模式的一個(gè)非常明顯的特征,就是代碼當(dāng)中出現(xiàn)了多重條件分支語句原茅,這種時(shí)候?yàn)榱舜a的擴(kuò)展性吭历,就可以選擇使用策略模式。
? 比如正面這樣的代碼擂橘,實(shí)現(xiàn)一個(gè)加減乘除運(yùn)算的操作晌区。
public class Operation { public static void main(String[] args) { binomialOperation(1,1,'+'); binomialOperation(1,3,'-'); binomialOperation(1,2,'*'); binomialOperation(1,1,'/'); binomialOperation(1,0,'/'); } public static int binomialOperation(int num1,int num2,char ch){ switch(ch){ case '+': return num1+num2; case '-': return num1+num2; case '*': return num1*num2; case '/': if(num2!=0){return num1/num2;} else { System.out.println("除數(shù)不能為0!"); } } return num2; } }
? 上面的代碼完全可以實(shí)現(xiàn)我們想要的功能,但是如果現(xiàn)在需求有變朗若,需要再增加一個(gè)‘與’和‘或’的二目運(yùn)算恼五;那在這種情況下,勢(shì)必需要去修改源碼哭懈,這樣就違背了開閉原則的思想灾馒。因此,使用策略模式遣总,將上面代碼修改為下列代碼睬罗。
//Strategy
public interface BinomialOperation {
public int operation(int num1,int num2);
}
public class AddOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
return num1+num2;
}
}
public class SubstractOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
return num1-num2;
}
}
public class MultiplyOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
return num1*num2;
}
}
public class DivideOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
if(0!=num2){
return num1/num2;
}else{
System.out.println("除數(shù)不能為0!");
return num2;
}
}
}
//Context
public class OperatioContext {
BinomialOperation binomialOperation;
public void setBinomialOperation(BinomialOperation binomialOperation) {
this.binomialOperation = binomialOperation;
}
public int useOperation(int num1,int num2){
return binomialOperation.operation(num1,num2);
}
}
public class Client {
public static void main(String[] args) {
OperatioContext oc = new OperatioContext();
oc.setBinomialOperation(new AddOperation());
oc.useOperation(1,2);
//......
}
}
代碼很簡(jiǎn)單彤避,就是將運(yùn)算類抽象出來傅物,形成一種策略,每個(gè)不同的運(yùn)算符對(duì)應(yīng)一個(gè)具體的策略琉预,并且實(shí)現(xiàn)自己的操作董饰。Strategy和Context相互作用以實(shí)現(xiàn)選定的算法。當(dāng)算法被調(diào)用時(shí)圆米,Context可以將自身作為一個(gè)參數(shù)傳遞給Strategy或者將所需要的數(shù)據(jù)都傳遞給Strategy卒暂,也就是說 `OperationContext`中`useOperation()`的`num1`和`num2`可以作為為`OperationContext`類的屬性,在使用過程中直接將`OperationContext`的對(duì)象作為一個(gè)參數(shù)傳遞給`Strategy`類即可娄帖。
通過策略模式的實(shí)現(xiàn)也祠,使得增加新的策略變得簡(jiǎn)單,但是其缺點(diǎn)就在于客戶必須了解 不同的策略近速。
-
**結(jié)構(gòu) ** Strategy.png
參與者
-
Strategy (如BinomialOperation)
定義所有支持的算法的公共接口诈嘿。Context使用這個(gè)接口來調(diào)用某具體的Strategy中定義的算法。
-
ConcreteStrategy(如AddOperation...)
根據(jù)Strategy接口實(shí)現(xiàn)具體算法削葱。
-
Context(如OperationContext)
- 需要一個(gè)或多個(gè)ConcreteStrategy來進(jìn)行配置奖亚,使用多個(gè)策略時(shí),這些具體的策略可能是不同的策略接口的實(shí)現(xiàn)析砸。比如昔字,實(shí)現(xiàn)一個(gè)工資計(jì)算系統(tǒng),工人身份有小時(shí)工首繁、周結(jié)工作郭、月結(jié)工,這種情況下弦疮,就可以將工人身份獨(dú)立為一個(gè)策略夹攒,再將工資支付計(jì)劃(用以判斷當(dāng)天是否為該工人支付工資日期)獨(dú)立為一個(gè)策略,這樣Context中就需要兩個(gè)策略來配置胁塞。
- 需要存放或者傳遞Strategy需要使用到的所有數(shù)據(jù)咏尝。
-
適用性
當(dāng)存在以下情況時(shí)堂湖,可以使用策略模式:
- 許多相關(guān)的類僅僅是行為有異∽赐粒“策略”提供了一種多個(gè)行為中的一些行為來配置一個(gè)類的方法无蜂。
- 需要使用一個(gè)算法的不同變體。例如蒙谓,你可以會(huì)定義一些反映不同空間/時(shí)間權(quán)衡的算法斥季,當(dāng)這些變體需要實(shí)現(xiàn)為一個(gè)算法的類層次時(shí),就可以采用策略模式累驮。
- 算法使用客戶不應(yīng)該知道的數(shù)據(jù)酣倾。可以采用策略模式避免暴露復(fù)雜的谤专、與算法相關(guān)的數(shù)據(jù)結(jié)構(gòu)躁锡。
- 一個(gè)類定義了多種行為,并且這些行為在這個(gè)類的操作中以多個(gè)條件語句的形式出現(xiàn)置侍。
相關(guān)模式
? Flyweight(享元模式)的共享機(jī)制可以減少需要生成過多Strategy對(duì)象映之,因?yàn)樵谑褂眠^程中,策略往往是可以共享使用的蜡坊。
-
思考
-
Strategy和Context之間的通信問題杠输。在Strategy和Contex接口中,必須使得ConcreteStrategy能夠有效的訪問它所需要的Context中的任何數(shù)據(jù)秕衙,反之亦然蠢甲。這種實(shí)現(xiàn)一般有兩種方式:
? 1)讓Context將數(shù)據(jù)放在參數(shù)中傳遞給Strategy——也就是說,將數(shù)據(jù)直接發(fā)送給Strategy据忘。這可以使得Strategy和Context之間解耦(印記耦合是可以接受的)鹦牛,但有可能會(huì)有一些Strategy不需要的數(shù)據(jù)。
? 2)將Context自身作為一個(gè)參數(shù)傳遞給Strategy勇吊,該Strategy顯示的向Context請(qǐng)求數(shù)據(jù)曼追,或者說明在Strategy中保留一個(gè)Context的引用,這樣便不需要再傳遞其他的數(shù)據(jù)了萧福。
讓Strategy成為可選的拉鹃。換名話說辈赋,在有些實(shí)現(xiàn)過程中鲫忍,客戶可以在不指定具體策略的情況下使用Context完成自己的工作。這是因?yàn)樵壳覀兛梢詾镃ontext指定一個(gè)默認(rèn)的Strategy的存在悟民,如果有指定Strategy就使用客戶指定的,如果沒有篷就,就使用默認(rèn)的射亏。
-
觀察者模式——對(duì)象行為型模式
-
意圖
? 定義對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新智润。
-
實(shí)例
? 觀察者模式很常見于圖形用戶界面當(dāng)中及舍,比如常見的Listener。觀察者模式可以使得應(yīng)用數(shù)據(jù)的類和負(fù)責(zé)界面表示的類可以各自獨(dú)立的復(fù)用窟绷。比如锯玛,當(dāng)界面當(dāng)中存在一個(gè)輸入表單,在我們對(duì)表單進(jìn)行輸入的時(shí)候兼蜈,界面上又會(huì)顯示這樣一個(gè)數(shù)據(jù)的柱狀圖攘残,以些來對(duì)比各項(xiàng)數(shù)據(jù)。其偽碼可以描述成下列這種形式:
Histogram
作為柱狀圖類只需要負(fù)責(zé)接收數(shù)據(jù)并且顯示出來为狸,InputForm
作為一個(gè)輸入表單歼郭。在這個(gè) 過程中,只要InputForm
中的數(shù)據(jù)發(fā)生變化辐棒,就相應(yīng)的改變Histogram
的顯示病曾。? 這種實(shí)現(xiàn)方式,明顯在
InputForm
中產(chǎn)生了一種強(qiáng)耦合漾根,如果顯現(xiàn)圖形發(fā)生變化知态,現(xiàn)在不需要顯示為一個(gè)柱狀圖而是一個(gè)餅狀圖,勢(shì)必又要去修改源碼。public class Histogram { public void draw(int[]nums){ for (int i:nums ) { System.out.print(i+" "); } } } public class InputForm { private int[] data; Histogram histogram; public InputForm(Histogram histogram){ this.histogram = histogram; show(); } public void change(int... data){ this.data = data; show(); } public void show(){ histogram.draw(data); } } public class Client { public static void main(String[] args) { InputForm inputForm = new InputForm(new Histogram()); inputForm.change(3,4,5); inputForm.change(5,12,13); } }
? 同時(shí)立叛,
InputForm
和顯示圖形之間的關(guān)系负敏,剛好符合觀察者模式所說的一個(gè)對(duì)象的狀態(tài)變化,引起其他對(duì)象的更新秘蛇,同時(shí)兼顧考慮開閉問題其做,可以將Histogram
和PieChart
公共特性提取出來,形成一個(gè)Graph
接口赁还。另外妖泄,有可能InputFrom
不只需要顯示一種圖表,而是需要同時(shí)將柱狀圖和餅狀圖顯示出來艘策,因此在InputFrom
中定義的是一個(gè)List的結(jié)構(gòu)來存放所有的相關(guān)顯示圖形蹈胡。//Observer public interface Graph { public void update(Input input); public void draw(); } public class Histogram implements Graph { private InputForm inputForm; public Histogram(InputForm inputForm){ this.inputForm = inputForm; } @Override public void update(Input inputForm) { if(this.inputForm == inputForm){ draw(); } } @Override public void draw(){ System.out.println("柱狀圖:"); for (int i: inputForm.getData()) { System.out.println(i+" "); } System.out.println(); } } public class PieChart implements Graph { private InputForm inputForm; public PieChart(InputForm inputForm){ this.inputForm = inputForm; this.inputForm.addGraph(this); draw(); } @Override public void update(Input inputForm) { if(this.inputForm == inputForm){ draw(); } } @Override @Override public void draw(){ System.out.println("餅狀圖:"); for (int i: inputForm.getData()) { System.out.println(i+" "); } System.out.println(); } }
? 在實(shí)際的應(yīng)用過程中,既然有輸入表單的形式朋蔫,也有可能以其他的形式輸入數(shù)據(jù)罚渐,為了以后的擴(kuò)展,可以將輸入形式抽象出來驯妄,形成一個(gè)
Input
接口荷并,以便后續(xù)的擴(kuò)展。//Subject 目標(biāo)對(duì)象 public interface Input { public void addGraph(Graph graph); public void removeGraph(Graph graph); public void notifyGraphs(); } public class InputForm implements Input { private int[] data; private List<Graph graphs = new List; public void change(int...data){ this.data = data; notifyGraphs(); } public int[] getData() { return data; } @Override public void addGraph(Graph graph){ graphs.add(graph); } @Override public void removeGraph(Graph graph){ graphs.remove(graph); } @Override public void notifyGraphs(){ for (Graph g:graphs ) { g.update(this); } } }
public class Client { public static void main(String[] args) { InputForm inputForm = new InputForm(); Histogram h = new Histogram(inputForm); PieChart p = new PieChart(inputForm); inputForm.change(1,5,6,9,8); inputForm.change(2,4,6,8); } }
-
結(jié)構(gòu)
Observer.png -
參與者
-
Subject(目標(biāo)青扔,如Input)
- 目標(biāo)需要知道自己所有的觀察者對(duì)象源织。
- 提供注冊(cè)和刪除觀察者對(duì)象的接口
-
Observer(觀察者翩伪,如Graph)
為那些在目標(biāo)發(fā)生變化時(shí)需要獲取通知的對(duì)象定義一個(gè)更新接口。
-
ConcreteSubject(具體目標(biāo)谈息,如InputForm)
- 將有關(guān)狀態(tài)(或數(shù)據(jù))存放到各ConcerteObserver對(duì)象中缘屹。
- 當(dāng)它的狀態(tài)發(fā)生改變時(shí),向它的各個(gè)觀察者發(fā)出通知侠仇。
-
ConcreteObserver(具體觀察者囊颅,如Histogram)
- 維護(hù)一個(gè)指向ConcerteSubject的引用,或者是有關(guān)狀態(tài)的引用傅瞻。
- 實(shí)現(xiàn)Observer的更新接口以使自身保存的目標(biāo)狀態(tài)與目標(biāo)狀態(tài)保持一致踢代。
-
-
適用性
在以下任一情況下可以使用觀察者模式:
- 當(dāng)一個(gè)抽象模型有兩個(gè)方面,其中一個(gè)方面依賴于另一個(gè)方面嗅骄。將這二者封裝在獨(dú)立的對(duì)象中以使它們可以各自獨(dú)立的改變和復(fù)用胳挎。
- 當(dāng)對(duì)一個(gè)對(duì)象的改變需要同時(shí)改變其它對(duì)象,而不知道具體有多少對(duì)象有待改變溺森。
- 當(dāng)一個(gè)對(duì)象必須通知其它對(duì)象慕爬,而它又不能假定其它對(duì)象是誰。換言之屏积,你不希望這些對(duì)象上緊耦合的医窿。
-
相關(guān)模式
? Mediator(中介者模式):通過封裝復(fù)雜的更新語義,可以使用一個(gè)ChangeManager來充當(dāng)目標(biāo)和觀察者之間的中介炊林。在目標(biāo)的狀態(tài)變化過程中姥卢,有些狀態(tài)變化可能只是中間臨時(shí)變化,而還未到最終結(jié)果渣聚,但這可能引起觀察者的更新独榴,這種頻繁的更新造成的就是通信代價(jià)和性能損失。因此奕枝,采用一個(gè)ChangeManager可以更好去管理更新操作棺榔。
? Singleton(單例模式):ChangeManager可以使用Singleton模式來保證它是唯一的并且是可全局訪問的。
-
思考
目標(biāo)與觀察者之間的映射隘道。一個(gè)目標(biāo)對(duì)象跟蹤它應(yīng)通知的觀察者的最簡(jiǎn)單方法是顯式地在目標(biāo)當(dāng)中保存對(duì)它們的引用症歇,但當(dāng)目標(biāo)過多而觀察者少時(shí),這樣存儲(chǔ)的結(jié)構(gòu)可能代價(jià)過高谭梗。其一個(gè)解決辦法就是用時(shí)間換空間忘晤,用一個(gè)關(guān)聯(lián)查找機(jī)制(例如一個(gè)hash表的形式)來維護(hù)目標(biāo)到觀察者的映射。這樣沒有觀察者的目標(biāo)自然不會(huì)產(chǎn)生存儲(chǔ)上的開銷默辨,但是由于關(guān)聯(lián)機(jī)制的存在德频,就相當(dāng)于在訪問觀察者的過程中多了一個(gè)步驟苍息,就增加了訪問觀察者的開銷缩幸。
一個(gè)目標(biāo)可以有很多觀察者壹置,一個(gè)觀察者也同樣可以觀察很多目標(biāo)。這種情況下表谊,就需要多觀察者的
update
接口作出一定的改變钞护,使得觀察者能夠知道是那個(gè)目標(biāo)對(duì)象發(fā)來通知。誰來觸發(fā)更新爆办。一是在對(duì)目標(biāo)狀態(tài)值進(jìn)行設(shè)定時(shí)难咕,自動(dòng)去調(diào)用通知信息。這樣客戶就不需要去調(diào)用
Notify()
距辆,缺點(diǎn)就在于多個(gè)連續(xù)的操作就會(huì)產(chǎn)生連續(xù)的更新余佃,造成效率低下。二是客戶自己選擇合適的情況下去調(diào)用Notify()
跨算,這種觸發(fā)方式優(yōu)點(diǎn)在于客戶可以在操作完成目標(biāo)對(duì)象之后爆土,一次性更新,避免了中間無用的更新诸蚕。缺點(diǎn)在于一旦客戶可能沒有調(diào)用通知步势,就容易出錯(cuò)。如何保證發(fā)出通知前目標(biāo)的狀態(tài)自身是一致的背犯。確保發(fā)出通知前目標(biāo)狀態(tài)一致這很重要坏瘩,因?yàn)橛^察者在更新狀態(tài)時(shí),需要查詢目標(biāo)的當(dāng)前狀態(tài)漠魏。這就需要在代碼序列中倔矾,保證通知是在目標(biāo)狀態(tài)修改完成之后進(jìn)行的,這時(shí)就可以采用Template Method來固定操作的順序柱锹。
小結(jié)
? 在這篇文章當(dāng)中破讨,沒有按照GOF對(duì)設(shè)計(jì)模式的分類來對(duì)設(shè)計(jì)模式進(jìn)行描述,而是在實(shí)例的基礎(chǔ)上奕纫,運(yùn)用重構(gòu)的技巧:從靜態(tài)到動(dòng)態(tài)提陶、從早綁定到晚綁定、從繼承到組合匹层、從編譯時(shí)依賴到運(yùn)行時(shí)依賴隙笆、從緊耦合到松耦合。通過這樣一種方式來理解設(shè)計(jì)模式升筏,尋找設(shè)計(jì)模式中的穩(wěn)定與變化撑柔。
? 在上面提到的三種模式中,它們對(duì)象間的綁定關(guān)系您访,都是動(dòng)態(tài)的铅忿,可以變化的,通過這樣的方式來實(shí)現(xiàn)協(xié)作對(duì)象之間的松耦合灵汪,這也是“組件協(xié)作”一個(gè)特點(diǎn)檀训。
? 還有就是關(guān)于耦合的理解柑潦,有的時(shí)候耦合是不可避免的,耦合的接受程度是相對(duì)而言的峻凫,這取決于我們?cè)趯?shí)現(xiàn)過程當(dāng)中對(duì)變化的封裝和穩(wěn)定的抽象折衷渗鬼,這也是我們學(xué)習(xí)設(shè)計(jì)模式的目的,就是如何利用設(shè)計(jì)模式來實(shí)現(xiàn)這樣一種取舍荧琼。
? 對(duì)設(shè)計(jì)模式細(xì)節(jié)描述過程譬胎,體現(xiàn)的是我在學(xué)習(xí)設(shè)計(jì)模式過程中的一種思路。學(xué)習(xí)一個(gè)設(shè)計(jì)模式命锄,首先要了解它是要干什么的堰乔。然后從一個(gè)例子出發(fā),去理解它脐恩,思考它的一個(gè)實(shí)現(xiàn)過程浩考。再然后,歸納它的結(jié)構(gòu)被盈,這個(gè)結(jié)構(gòu)不僅僅是類圖析孽,還包括類圖中的各個(gè)協(xié)作者是需要完成什么的功能、提供什么樣的接口只怎、要保存哪些數(shù)據(jù)以及各各協(xié)作者之間是如何協(xié)作的袜瞬,或者說是依賴關(guān)系是怎樣的。最后身堡,再考慮與其他模式的搭配邓尤,思考模式的實(shí)現(xiàn)細(xì)節(jié)。
這里呢贴谎,暫時(shí)只寫出了三種模式汞扎,后續(xù)的過程中,將會(huì)一一地介紹其他的模式擅这。
最后澈魄,最近很多小伙伴找我要Linux學(xué)習(xí)路線圖,于是我根據(jù)自己的經(jīng)驗(yàn)仲翎,利用業(yè)余時(shí)間熬夜肝了一個(gè)月痹扇,整理了一份電子書。無論你是面試還是自我提升溯香,相信都會(huì)對(duì)你有幫助鲫构!目錄如下:
免費(fèi)送給大家,只求大家金指給我點(diǎn)個(gè)贊玫坛!
電子書 | Linux開發(fā)學(xué)習(xí)路線圖
也希望有小伙伴能加入我结笨,把這份電子書做得更完美!
有收獲?希望老鐵們來個(gè)三連擊炕吸,給更多的人看到這篇文章
推薦閱讀: