從封裝變化的角度看設計模式——組件協作

来源:https://www.cnblogs.com/yychuyu/archive/2020/07/12/13289913.html
-Advertisement-
Play Games

什麼是設計模式 ​ 要瞭解設計模式,首先得清楚什麼是模式。什麼是模式?模式即解決一類問題的方法論,簡單得來說,就是將解決某類問題的方法歸納總結到理論高度,就形成了模式。 ​ 設計模式就是將代碼設計經驗歸納總結到理論高度而形成的。其目的就在於:1)可重用代碼,2)讓代碼更容易為他人理解,3)保證代碼的 ...


什麼是設計模式

​ 要瞭解設計模式,首先得清楚什麼是模式。什麼是模式?模式即解決一類問題的方法論,簡單得來說,就是將解決某類問題的方法歸納總結到理論高度,就形成了模式。

​ 設計模式就是將代碼設計經驗歸納總結到理論高度而形成的。其目的就在於:1)可重用代碼,2)讓代碼更容易為他人理解,3)保證代碼的可靠性。

​ 使用面向對象的語言很容易,但是做到面向對象卻很難。更多人用的是面向對象的語言寫出結構化的代碼,想想自己編寫的代碼有多少是不用修改源碼可以真正實現重用,或者可以實現拿來主義。這是一件很正常的事,我在學習過程當中,老師們總是在說c到c++的面向對象是一種巨大的進步,面向對象也是極為難以理解的存在;而在開始的學習過程中,我發現c++和c好像差別也不大,不就是多了一個類和對象嗎?但隨著愈發深入的學習使我發現,事實並不是那麼簡單,老師們舉例時總是喜歡用到簡單的對象群體,比如:人,再到男人、女人,再到擁有具體家庭身份的父親、母親、孩子。用這些來說明類、對象、繼承......似乎都顯得面向對象是一件輕而易舉的事。

​ 但事實真是如此嗎?封裝、粒度、依賴關係、靈活性、性能、演化、復用等等,當這些在一個系統當中交錯相連,互相耦合,甚至有些東西還互相衝突時,你會發現自己可能連將系統對象化都是那麼的困難。

​ 而在解決這些問題的過程當中,也就慢慢形成了一套被反覆使用、為多數人知曉、再由人分類編目的代碼設計經驗總結——設計模式。

設計原則

​ 模式既然作為一套解決方案,自然不可能是沒有規律而言的,而其所遵循的內在規律就是設計原則。在學習設計模式的過程當中,不能脫離原則去看設計模式,而是應該透過設計模式去理解設計原則,只有深深地把握了設計原則,才能寫出真正的面向對象代碼,甚至創造自己的模式。

  1. 開閉原則(Open Close Principle)

    ​ 開閉原則的意思是:對擴展開放,對修改關閉。在程式需要進行拓展的時候,不要去修改原有的代碼。這樣是為了使程式的擴展性更好,更加易於維護和升級。而想要達到這樣的效果,就需要使用介面和抽象類。

  2. 里氏替換原則(Liskov Substitution Principle)

    ​ 里氏替換原則中說,任何基類可以出現的地方,子類一定可以出現。也就是說只有當派生類可以替換掉基類,且軟體單位的功能不受到影響時,基類才能真正被覆用,而派生類也能夠在基類的基礎上增加新的行為。里氏代換原則是對開閉原則的補充。實現開閉原則的關鍵步驟就是抽象化,而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範。

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

    ​ 依賴倒置原則是開閉原則的基礎,具體內容:抽象不應該依賴具體,而是具體應當依賴抽象;高層模塊不應該依賴底層模塊,而是高層和底層模塊都要依賴抽象。因為抽象才是穩定的,這個原則想要說明的就是針對介面編程。

  4. 介面分離原則(Interface Segregation Principle)

    ​ 這個原則的意思是:使用多個隔離的介面,比使用單個介面要好。它還有另外一個意思是:降低類之間的耦合度。這個原則所要求的就是儘量將介面最小化,避免一個介面當中擁有太多不相關的功能。

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

    ​ 最少知道原則是指:如果兩個軟體實體無須直接通信,那麼就不應當發生直接的相互調用,可以通過第三方轉發該調用。其目的是降低類之間的耦合度,提高模塊的相對獨立性。迪米特法則在解決訪問耦合方面有著很大的作用,但是其本身的應用也有著一個很大的缺點,就是對象之間的通信造成性能的損失,這是在使用過程中,需要去折衷考慮的。

  6. 組合復用原則(Composite Reuse Principle)

    ​ 組合復用原則或者說組合優先原則,也就是在進行功能復用的過程當中,組合往往是比繼承更好的選擇。這是因為繼承的形式會使得父類的實現細節對子類可見,從而違背了封裝的目的。

  7. 單一職責原則(Single Responsibility Principle)

    ​ 一個類只允許有一個職責,即只有一個導致該類變更的原因。類職責的變化往往就是導致類變化的原因:也就是說如果一個類具有多種職責,就會有多種導致這個類變化的原因,從而導致這個類的維護變得困難。

​ 設計模式是設計原則在應用體現,設計原則是解決面向對象問題處理方法。在面對訪問耦合的情況下,有針對介面編程、介面分離、迪米特法則;處理繼承耦合問題,有里氏替換原則、優先組合原則;在保證類的內聚時,可以採用單一職責原則、集中類的信息與行為。這一系列的原則都是為了一個目的——儘可能的實現開閉。設計模式不是萬能的,它是設計原則互相取捨的成果,而學習設計模式是如何抓住變化和穩定的界線才是設計模式的真諦。

GOF-23 模式分類

​ 從目的來看,即模式是用來完成什麼工作的;可以劃分為創建型、結構型和行為型。創建型模式與對象的創建有關,結構型模式處理類或對象的組合,行為型模式對類和對象怎樣分配職責進行描述。

​ 從範圍來看,即模式是作用於類還是對象;可以劃分為類模式和對象模式。類模式處理類和子類之間的關係,這些關係通過繼承建立,是靜態的,在編譯時刻就確定下來了;對象模式處理對象間的關係,這些關係可以在運行時刻變化,更加具有動態性。

組合之下,就產生了以下六種模式類別:

  1. 類創建型模式:將對象的創建工作延遲到子類中。

  2. 對象創建型模式:將對象的創建延工作遲到另一個對象的中。

  3. 類結構型模式:使用繼承機制來組合類。

  4. 對象創建型模式:描述對象的組裝形式。

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

  6. 對象行為型模式:描述了一組對象怎樣協作完成單個對象所無法完成的任務。

從封裝變化的角度來看

​ GOF(“四人組”)對設計模式的分類更多的是從用途方法進行劃分,而現在,我們希望從設計模式中變化和穩定結構分隔上來理解所有的設計模式,或許有著不同的收穫。

​ 首先要明白的是,獲得最大限度復用的關鍵在於對新需求和已有需求發生變化的預見性,這也就要求系統設計能夠相應地改進。而設計模式可以確保系統以特定的方式變化,從而避免系統的重新設計,並且設計模式同樣允許系統結構的某個方面的變化獨立於其他方面,這樣就在一定程度上加強了系統的健壯性。

​ 根據封裝變化,可以將設計模式劃分為:組件協作、單一職責、對象創建、對象性能、介面隔離、狀態變化、數據結構、行為變化以及領域問題等等。

設計模式之組件協作

​ 現代軟體專業分工之後的第一個結果就是“框架與應用程式的劃分”,“組件協作”就是通過晚期綁定,來實現框架與應用程式之間的松耦合,是二者之間協作時常用的模式。其典型模式就是模板方法、策略模式和觀察者。

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

​ 定義一個操作中的演算法的骨架,並將其中一些步驟的實現延遲到子類中。模板方法使得子類可以重定義一個演算法的步驟而不會改變演算法的結構。

  1. 實例

​ 程式開發庫和應用程式之間的調用。假設現在存在一個開發庫,其內容是實現對一個文件或信息的操作,操作包含:open、read、operation、commit、close。但是呢!只有open、commit、close是確定的,其中read需要根據具體的operation來確定讀取方式,所以這兩個方法是需要開發人員自己去實現的。

​ 那我們第一次的實現可能就是這種方式:

//標準庫實現
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);
    }
}
//應用程式的實現
public class MyApplication {
    public void read(String s,String type){
        System.out.println("使用"+type+"方式read: "+s);
    }
    public void operation(){
        System.out.println("operation");
    }
}
//或者這樣實現
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");
    }
}
//這裡兩種實現方式的代碼調用寫在一起,就不分開了。
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);
    }
}

​ 這種實現,無論是方式1還是方式2,對於僅僅是作為應用來說,當然是可以的。其問題主要在什麼地方呢?就方式1 而言,他是必須要使用者瞭解開發庫和應用程式兩個類,才能夠正確的去應用。

​ 方式2相較於方式1,使用更加的簡單些,但是仍然有不完善的地方,就是調用者,需要知道各個方法的執行順序,這也是1和2共同存在的問題。而這剛好就是Template Method發揮的時候了,一系列操作有著明確的順序,並且有著部分的操作不變,剩下的操作待定。

//按照Template Method結構可以將標準庫作出如下修改
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);
    }
}

​ 在修改過程中,將原來的類修改成了抽象類,並且新增了兩個抽象方法和一個doOperation()。通過使用抽象操作定義一個演算法中的一些步驟,模板方法確定了它們的先後順序,但它允許Library和Application子類改變這些具體的步驟以滿足它們各自的需求,並且還對外隱藏了演算法的實現。當然,如果標準庫中的不變方法不能被重定義,那麼就應該將其設置為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");
    }
}

​ 模板方法的使用在類庫當中極為常見,尤其是在c++的類庫當中,它是一種基本的代碼復用技術。這種實現方式,產生了一種反向的控制結構,或者我們稱之為“好萊塢法則”,即“別找我們,我們找你”;換名話說,這種反向控制結構就是父類調用了子類的操作(父類中的doOperation()調用了子類實現的read()operation(),因為在平時,我們的繼承代碼復用更多的是調用子類調用父類的操作。

  1. 結構

    templateMethod.png

  2. 參與者

    • AbstractClass(StdLibrary)

      定義抽象的原語操作(可變部分)。

      實現一個模板方法(templateMethod()),定義演算法的骨架。

    • ConcreteClass(具體的實現類,如MyApplication)

      實現原語操作以完成演算法中與特定子類相關的步驟。

    除了以上參與者之外,還可以有OperatedObject這樣一個參與者即被操作對象。比如對文檔的操作,文檔又有不同的類型,如pdf、word、txt等等;這種情況下,就需要根據不同的文檔類型,定製不同的操作,即一個ConcreteClass對應一個OperatedObject,相當於對結構當中由一個特定操作對象,擴展到多個操作對象,並且每個操作對象對應一個模板方法子類。

  3. 適用性

    對於模板方法的特性,其可以應用於下列情況:

    • 一次性實現一個演算法的不變部分,並將可變的行為留給子類來實現。
    • 各子類中公共的行為應被提取出來並集中到一個公共父類中,以避免代碼重覆。重構方式即為首先識別現有代碼中的不同之處,並且將不同之處分離為新的操作。最後用一個模板方法調用這些新的操作,來替換這些不同的代碼。
    • 控制子類的擴展。模板方法只有特定點調用"hook"操作,這樣就只允許在這些擴展點進行相應的擴展。
  4. 相關模式

    ​ Factory Method經常被Template Method所調用。比如在參與者當中提到的,如果需要操作不同的文件對象,那麼在操作的過程中就需要read()方法返回不同的文件對象,而這個read()方法不正是一個Factory Method。

    ​ Strategy:Template Method使用繼承來改變演算法的一部分,而Strategy使用委托來改變整個演算法。

  5. 思考

    • 訪問控制 在定義模板的時候,除了簡單的定義原語操作和演算法骨架之外,操作的控制權也是需要考慮的。原語操作是可以被重定義的,所以不能設置為final,還有原語操作能否為其他不相關的類所調用,如果不能則可以設置為protected或者default。模板方法一般是不讓子類重定義的,因此就需要設置為final.
    • 原語操作數量 定義模板方法的一個重要目的就是儘量減少一個子類具體實現該演算法時,必須重定義的那些原語操作的數目。因為,需要重定義的操作越多,應用程式就越冗長。
    • 命名約定 對於需要重定義的操作可以加上一個特定的首碼以便開發人員識別它們。
    • hook操作 hook操作就是指那些在模板方法中定義的可以重定義的操作,子類在必要的時候可以進行擴展。當然,如果可以使用父類的操作,不擴展也是可以的;因此,在Template Method中,應該去指明哪些操作是不能被重定義的、哪些是hook(可以被重定義)以及哪些是抽象操作(必須被重定義)。
策略模式——對象行為型模式
  1. 意圖

    ​ 定義一系列的演算法,把它們一個個封裝起來,並且使它們可相互替換。Strategy使得演算法可以獨立於使用它的客戶而變化。

  2. 實例

    ​ 策略模式是一種非常經典的設計模式,可能也是大家經常所見到和使用的設計模式;重構過程中選擇使用策略模式的一個非常明顯的特征,就是代碼當中出現了多重條件分支語句,這種時候為了代碼的擴展性,就可以選擇使用策略模式。

    ​ 比如正面這樣的代碼,實現一個加減乘除運算的操作。

     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("除數不能為0!");
                    }
            }
            return num2;
        }
    }
    

    ​ 上面的代碼完全可以實現我們想要的功能,但是如果現在需求有變,需要再增加一個‘與’和‘或’的二目運算;那在這種情況下,勢必需要去修改源碼,這樣就違背了開閉原則的思想。因此,使用策略模式,將上面代碼修改為下列代碼。

 //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("除數不能為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);
         //......
     }
 }
	代碼很簡單,就是將運算類抽象出來,形成一種策略,每個不同的運算符對應一個具體的策略,並且實現自己的操作。Strategy和Context相互作用以實現選定的演算法。當演算法被調用時,Context可以將自身作為一個參數傳遞給Strategy或者將所需要的數據都傳遞給Strategy,也就是說 `OperationContext`中`useOperation()`的`num1`和`num2`可以作為為`OperationContext`類的屬性,在使用過程中直接將`OperationContext`的對象作為一個參數傳遞給`Strategy`類即可。

	通過策略模式的實現,使得增加新的策略變得簡單,但是其缺點就在於客戶必須瞭解 不同的策略。
  1. **結構 ** Strategy.png

  2. 參與者

  • Strategy (如BinomialOperation)

    定義所有支持的演算法的公共介面。Context使用這個介面來調用某具體的Strategy中定義的演算法。

  • ConcreteStrategy(如AddOperation...)

    根據Strategy介面實現具體演算法。

  • Context(如OperationContext)

    • 需要一個或多個ConcreteStrategy來進行配置,使用多個策略時,這些具體的策略可能是不同的策略介面的實現。比如,實現一個工資計算系統,工人身份有小時工、周結工、月結工,這種情況下,就可以將工人身份獨立為一個策略,再將工資支付計劃(用以判斷當天是否為該工人支付工資日期)獨立為一個策略,這樣Context中就需要兩個策略來配置。
    • 需要存放或者傳遞Strategy需要使用到的所有數據。
  1. 適用性

    當存在以下情況時,可以使用策略模式:

    • 許多相關的類僅僅是行為有異。“策略”提供了一種多個行為中的一些行為來配置一個類的方法。
    • 需要使用一個演算法的不同變體。例如,你可以會定義一些反映不同空間/時間權衡的演算法,當這些變體需要實現為一個演算法的類層次時,就可以採用策略模式。
    • 演算法使用客戶不應該知道的數據。可以採用策略模式避免暴露複雜的、與演算法相關的數據結構。
    • 一個類定義了多種行為,並且這些行為在這個類的操作中以多個條件語句的形式出現。
  2. 相關模式

​ Flyweight(享元模式)的共用機制可以減少需要生成過多Strategy對象,因為在使用過程中,策略往往是可以共用使用的。

  1. 思考

    • Strategy和Context之間的通信問題。在Strategy和Contex介面中,必須使得ConcreteStrategy能夠有效的訪問它所需要的Context中的任何數據,反之亦然。這種實現一般有兩種方式:

      ​ 1)讓Context將數據放在參數中傳遞給Strategy——也就是說,將數據直接發送給Strategy。這可以使得Strategy和Context之間解耦(印記耦合是可以接受的),但有可能會有一些Strategy不需要的數據。

      ​ 2)將Context自身作為一個參數傳遞給Strategy,該Strategy顯示的向Context請求數據,或者說明在Strategy中保留一個Context的引用,這樣便不需要再傳遞其他的數據了。

    • 讓Strategy成為可選的。換名話說,在有些實現過程中,客戶可以在不指定具體策略的情況下使用Context完成自己的工作。這是因為,我們可以為Context指定一個預設的Strategy的存在,如果有指定Strategy就使用客戶指定的,如果沒有,就使用預設的。

觀察者模式——對象行為型模式
  1. 意圖

    ​ 定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。

  2. 實例

    ​ 觀察者模式很常見於圖形用戶界面當中,比如常見的Listener。觀察者模式可以使得應用數據的類和負責界面表示的類可以各自獨立的復用。比如,當界面當中存在一個輸入表單,在我們對錶單進行輸入的時候,界面上又會顯示這樣一個數據的柱狀圖,以些來對比各項數據。其偽碼可以描述成下列這種形式:Histogram作為柱狀圖類只需要負責接收數據並且顯示出來,InputForm作為一個輸入表單。在這個 過程中,只要InputForm中的數據發生變化,就相應的改變Histogram 的顯示。

    ​ 這種實現方式,明顯在InputForm中產生了一種強耦合,如果顯現圖形發生變化,現在不需要顯示為一個柱狀圖而是一個餅狀圖,勢必又要去修改源碼。

    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);
        }
    }
    

    ​ 同時,InputForm 和顯示圖形之間的關係,剛好符合觀察者模式所說的一個對象的狀態變化,引起其他對象的更新,同時兼顧考慮開閉問題,可以將HistogramPieChart公共特性提取出來,形成一個Graph介面。另外,有可能InputFrom不只需要顯示一種圖表,而是需要同時將柱狀圖和餅狀圖顯示出來,因此在InputFrom中定義的是一個List的結構來存放所有的相關顯示圖形。

    //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();
        }
    }
    
    

    ​ 在實際的應用過程中,既然有輸入表單的形式,也有可能以其他的形式輸入數據,為了以後的擴展,可以將輸入形式抽象出來,形成一個Input介面,以便後續的擴展。

    //Subject 目標對象
    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. 結構

    Observer.png

  4. 參與者

    • Subject(目標,如Input)

      • 目標需要知道自己所有的觀察者對象。
      • 提供註冊和刪除觀察者對象的介面
    • Observer(觀察者,如Graph)

      為那些在目標發生變化時需要獲取通知的對象定義一個更新介面。

    • ConcreteSubject(具體目標,如InputForm)

      • 將有關狀態(或數據)存放到各ConcerteObserver對象中。
      • 當它的狀態發生改變時,向它的各個觀察者發出通知。
    • ConcreteObserver(具體觀察者,如Histogram)

      • 維護一個指向ConcerteSubject的引用,或者是有關狀態的引用。
      • 實現Observer的更新介面以使自身保存的目標狀態與目標狀態保持一致。
  5. 適用性

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

    • 當一個抽象模型有兩個方面,其中一個方面依賴於另一個方面。將這二者封裝在獨立的對象中以使它們可以各自獨立的改變和復用。
    • 當對一個對象的改變需要同時改變其它對象,而不知道具體有多少對象有待改變。
    • 當一個對象必須通知其它對象,而它又不能假定其它對象是誰。換言之,你不希望這些對象上緊耦合的。
  6. 相關模式

    ​ Mediator(中介者模式):通過封裝複雜的更新語義,可以使用一個ChangeManager來充當目標和觀察者之間的中介。在目標的狀態變化過程中,有些狀態變化可能只是中間臨時變化,而還未到最終結果,但這可能引起觀察者的更新,這種頻繁的更新造成的就是通信代價和性能損失。因此,採用一個ChangeManager可以更好去管理更新操作。

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

  7. 思考

    • 目標與觀察者之間的映射。一個目標對象跟蹤它應通知的觀察者的最簡單方法是顯式地在目標當中保存對它們的引用,但當目標過多而觀察者少時,這樣存儲的結構可能代價過高。其一個解決辦法就是用時間換空間,用一個關聯查找機制(例如一個hash表的形式)來維護目標到觀察者的映射。這樣沒有觀察者的目標自然不會產生存儲上的開銷,但是由於關聯機制的存在,就相當於在訪問觀察者的過程中多了一個步驟,就增加了訪問觀察者的開銷。

    • 一個目標可以有很多觀察者,一個觀察者也同樣可以觀察很多目標。這種情況下,就需要多觀察者的update介面作出一定的改變,使得觀察者能夠知道是那個目標對象發來通知。

    • 誰來觸發更新。一是在對目標狀態值進行設定時,自動去調用通知信息。這樣客戶就不需要去調用Notify(),缺點就在於多個連續的操作就會產生連續的更新,造成效率低下。二是客戶自己選擇合適的情況下去調用Notify(),這種觸發方式優點在於客戶可以在操作完成目標對象之後,一次性更新,避免了中間無用的更新。缺點在於一旦客戶可能沒有調用通知,就容易出錯。

    • 如何保證發出通知前目標的狀態自身是一致的。確保發出通知前目標狀態一致這很重要,因為觀察者在更新狀態時,需要查詢目標的當前狀態。這就需要在代碼序列中,保證通知是在目標狀態修改完成之後進行的,這時就可以採用Template Method來固定操作的順序。

小結

​ 在這篇文章當中,沒有按照GOF對設計模式的分類來對設計模式進行描述,而是在實例的基礎上,運用重構的技巧:從靜態到動態、從早綁定到晚綁定、從繼承到組合、從編譯時依賴到運行時依賴、從緊耦合到松耦合。通過這樣一種方式來理解設計模式,尋找設計模式中的穩定與變化。

​ 在上面提到的三種模式中,它們對象間的綁定關係,都是動態的,可以變化的,通過這樣的方式來實現協作對象之間的松耦合,這也是“組件協作”一個特點。

​ 還有就是關於耦合的理解,有的時候耦合是不可避免的,耦合的接受程度是相對而言的,這取決於我們在實現過程當中對變化的封裝和穩定的抽象折衷,這也是我們學習設計模式的目的,就是如何利用設計模式來實現這樣一種取捨。

​ 對設計模式細節描述過程,體現的是我在學習設計模式過程中的一種思路。學習一個設計模式,首先要瞭解它是要乾什麼的。然後從一個例子出發,去理解它,思考它的一個實現過程。再然後,歸納它的結構,這個結構不僅僅是類圖,還包括類圖中的各個協作者是需要完成什麼的功能、提供什麼樣的介面、要保存哪些數據以及各各協作者之間是如何協作的,或者說是依賴關係是怎樣的。最後,再考慮與其他模式的搭配,思考模式的實現細節。

這裡呢,暫時只寫出了三種模式,後續的過程中,將會一一地介紹其他的模式。


公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Spring Cloud 版本重大變革,變更了版本號的命名方式。 舊版命名方式看這篇: Spring Cloud 多版本怎麼選擇? 從 Spring Cloud 2020.0.0-M1 開始,Spring Cloud 廢除了這種英國倫敦地鐵站的命名方式,而使用了全新的 "日曆化" 版本命名方式。 官 ...
  • 前言: 最近在關註微服務,在 eShop On Containers 項目中存在一個API網關項目,引起想深入瞭解下它的興趣。 一、API網關是什麼 API網關是微服務架構中的唯一入口,它提供一個單獨且統一的API入口用於訪問內部一個或多個API。它可以具有身份驗證,監控,負載均衡,緩存,請求分片與 ...
  • 最近兩天在Linux中調試.NET Core應用程式,同時我發現在Linux中調試.NET Core應用程式並不容易。一直習慣在Visual Studio中進行編碼和調試。現在我想的是可以簡單快速的測試.NET Core應用在Linux。所以通過本篇文章我們能瞭解到如何在Windows中使用Visu ...
  • DirectX與WPF DirectX DirectX(Direct eXtension,簡稱DX)是由微軟公司創建的多媒體編程介面,是一種應用程式介面(API)。DirectX可以讓以windows為平臺的游戲或多媒體程式獲得更高的執行效率,加強3D圖形和聲音效果,並提供設計人員一個共同的硬體驅動 ...
  • 頭文件添加方法:工程 屬性 配置屬性 c/c++ 常規 附加包含目錄(Additional Include Directories):加上頭文件存放目錄。註意:(1)路徑必須指向頭文件所在的子文件夾,而不能直到父文件夾就結束(2)每個路徑不需要加上雙引號,輸入了之後,vs會自動加上雙引號,如果自己加 ...
  • 動態引入技術的設計,對我們來說非常重要。 同時也說明動態語言的使用對我們來說也是非常重要。 沒有動態語言的支撐,有些想法可能不容易實現,或者有替代方案,可能會花更大的代價。 前端開發框架我們規劃設計了 apis文件夾 這個文件夾是用來存放所有的api定義的 規範要求 所有的api定義都是名動詞 同時 ...
  • 最近GRPC很火,感覺整RPC不用GRPC都快跟不上時髦了。 gRPC設計 gRPC是一種與語言無關的高性能遠程過程調用 (RPC) 框架。剛好需要使用一個的RPC應用系統,自然而然就盯上了它,但是它真能夠解決所有問題嗎?不見得,先看看他的優點: gRPC的主要優點: 現代高性能輕量級 RPC 框架 ...
  • 瞭解到了OrchardCore主要由兩個中間件(ModularTenantContainerMiddleware和ModularTenantRouterMiddleware)構成,下麵開始瞭解ModularTenantContainerMiddleware中間件第一行代碼。 瞭解asp.net co ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...