從封裝變化的角度看設(shè)計(jì)模式——組件協(xié)作

什么是設(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)造自己的模式歼指。

  1. 開閉原則(Open Close Principle)

    ? 開閉原則的意思是:對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉甥雕。在程序需要進(jìn)行拓展的時(shí)候踩身,不要去修改原有的代碼。這樣是為了使程序的擴(kuò)展性更好犀农,更加易于維護(hù)和升級(jí)惰赋。而想要達(dá)到這樣的效果,就需要使用接口和抽象類呵哨。

  1. 里氏替換原則(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ī)范惯雳。

  2. 依賴倒置原則(Dependence Inversion Principle)

    ? 依賴倒置原則是開閉原則的基礎(chǔ),具體內(nèi)容:抽象不應(yīng)該依賴具體鸿摇,而是具體應(yīng)當(dāng)依賴抽象石景;高層模塊不應(yīng)該依賴底層模塊,而是高層和底層模塊都要依賴抽象拙吉。因?yàn)槌橄蟛攀欠€(wěn)定的潮孽,這個(gè)原則想要說明的就是針對(duì)接口編程。

  3. 接口分離原則(Interface Segregation Principle)

    ? 這個(gè)原則的意思是:使用多個(gè)隔離的接口筷黔,比使用單個(gè)接口要好往史。它還有另外一個(gè)意思是:降低類之間的耦合度。這個(gè)原則所要求的就是盡量將接口最小化佛舱,避免一個(gè)接口當(dāng)中擁有太多不相關(guān)的功能椎例。

  4. 迪米特法則,又稱最少知道原則(Demeter Principle)

    ? 最少知道原則是指:如果兩個(gè)軟件實(shí)體無須直接通信请祖,那么就不應(yīng)當(dāng)發(fā)生直接的相互調(diào)用粟矿,可以通過第三方轉(zhuǎn)發(fā)該調(diào)用。其目的是降低類之間的耦合度损拢,提高模塊的相對(duì)獨(dú)立性陌粹。迪米特法則在解決訪問耦合方面有著很大的作用,但是其本身的應(yīng)用也有著一個(gè)很大的缺點(diǎn)福压,就是對(duì)象之間的通信造成性能的損失掏秩,這是在使用過程中,需要去折衷考慮的荆姆。

  5. 組合復(fù)用原則(Composite Reuse Principle)

    ? 組合復(fù)用原則或者說組合優(yōu)先原則蒙幻,也就是在進(jìn)行功能復(fù)用的過程當(dāng)中,組合往往是比繼承更好的選擇胆筒。這是因?yàn)槔^承的形式會(huì)使得父類的實(shí)現(xiàn)細(xì)節(jié)對(duì)子類可見邮破,從而違背了封裝的目的。

  6. 單一職責(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)生了以下六種模式類別:

  1. 類創(chuàng)建型模式:將對(duì)象的創(chuàng)建工作延遲到子類中功氨。

  2. 對(duì)象創(chuàng)建型模式:將對(duì)象的創(chuàng)建延工作遲到另一個(gè)對(duì)象的中序苏。

  3. 類結(jié)構(gòu)型模式:使用繼承機(jī)制來組合類。

  4. 對(duì)象創(chuàng)建型模式:描述對(duì)象的組裝形式捷凄。

  5. 類行為型模式:使用繼承描述算法和控制流杠览。

  6. 對(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í)常用的模式漆诽。其典型模式就是模板方法、策略模式和觀察者锣枝。

模板方法——類行為型模式
  1. 意圖

? 定義一個(gè)操作中的算法的骨架拴泌,并將其中一些步驟的實(shí)現(xiàn)延遲到子類中。模板方法使得子類可以重定義一個(gè)算法的步驟而不會(huì)改變算法的結(jié)構(gòu)惊橱。

  1. 實(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)用父類的操作嗓节。

  1. 結(jié)構(gòu)

    templateMethod.png
  2. 參與者

    • 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è)模板方法子類砸喻。

  3. 適用性

    對(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ò)展疆股。
  4. 相關(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è)算法永毅。

  5. 思考

    • 訪問控制 在定義模板的時(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ì)象行為型模式
  1. 意圖

    ? 定義一系列的算法,把它們一個(gè)個(gè)封裝起來速缨,并且使它們可相互替換锌妻。Strategy使得算法可以獨(dú)立于使用它的客戶而變化。

  2. 實(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)就在于客戶必須了解 不同的策略近速。
  1. **結(jié)構(gòu) **
    Strategy.png
  2. 參與者

  • 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ù)咏尝。
  1. 適用性

    當(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)置侍。
  2. 相關(guān)模式

? Flyweight(享元模式)的共享機(jī)制可以減少需要生成過多Strategy對(duì)象映之,因?yàn)樵谑褂眠^程中,策略往往是可以共享使用的蜡坊。

  1. 思考

    • 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ì)象行為型模式
  1. 意圖

    ? 定義對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新智润。

  2. 實(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í)兼顧考慮開閉問題其做,可以將HistogramPieChart公共特性提取出來,形成一個(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);
        }
    }
    
  3. 結(jié)構(gòu)

    Observer.png
  4. 參與者

    • 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)保持一致踢代。
  5. 適用性

    在以下任一情況下可以使用觀察者模式:

    • 當(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ì)象上緊耦合的医窿。
  6. 相關(guān)模式

    ? Mediator(中介者模式):通過封裝復(fù)雜的更新語義,可以使用一個(gè)ChangeManager來充當(dāng)目標(biāo)和觀察者之間的中介炊林。在目標(biāo)的狀態(tài)變化過程中姥卢,有些狀態(tài)變化可能只是中間臨時(shí)變化,而還未到最終結(jié)果渣聚,但這可能引起觀察者的更新独榴,這種頻繁的更新造成的就是通信代價(jià)和性能損失。因此奕枝,采用一個(gè)ChangeManager可以更好去管理更新操作棺榔。

    ? Singleton(單例模式):ChangeManager可以使用Singleton模式來保證它是唯一的并且是可全局訪問的。

  7. 思考

    • 目標(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è)三連擊炕吸,給更多的人看到這篇文章

推薦閱讀:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末伐憾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子算途,更是在濱河造成了極大的恐慌塞耕,老刑警劉巖蚀腿,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嘴瓤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡莉钙,警方通過查閱死者的電腦和手機(jī)廓脆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來磁玉,“玉大人停忿,你說我怎么就攤上這事∥蒙。” “怎么了席赂?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)时迫。 經(jīng)常有香客問我颅停,道長(zhǎng),這世上最難降的妖魔是什么掠拳? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任癞揉,我火速辦了婚禮,結(jié)果婚禮上溺欧,老公的妹妹穿的比我還像新娘喊熟。我一直安慰自己,他們只是感情好姐刁,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布芥牌。 她就那樣靜靜地躺著,像睡著了一般聂使。 火紅的嫁衣襯著肌膚如雪胳泉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天岩遗,我揣著相機(jī)與錄音扇商,去河邊找鬼。 笑死宿礁,一個(gè)胖子當(dāng)著我的面吹牛案铺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梆靖,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼控汉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼笔诵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起姑子,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤乎婿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后街佑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谢翎,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年沐旨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了森逮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡磁携,死狀恐怖褒侧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谊迄,我是刑警寧澤闷供,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站统诺,受9級(jí)特大地震影響歪脏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜篙议,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一唾糯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鬼贱,春花似錦移怯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至姻乓,卻和暖如春嵌溢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蹋岩。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工赖草, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人剪个。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓秧骑,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乎折,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350