Files
Obsidian-Main/20.01. Programming/Design Pattern.md

9.2 KiB
Raw Permalink Blame History

tags, aliases, date, time, description
tags aliases date time description
2022-05-26 15:12:04

策略模式Strategy

策略模式可以提供不一樣的演算法,但是又不用更改程式。 以常見的鴨子為例有一個基礎類別Duck如何衍生出會飛得鴨子跟不會飛的鴨子抑或是會叫的跟不會叫的 第一部是將會變動的部份分離出來,讓鴨子類別不需要去在乎飛跟叫的問題。 再來是把飛跟叫的部份包裝成另一個class並以之為基礎類別來實做出「實際的類別」。 ^e59e9f

以一般C++的override方法會用的方式大致是這樣

  • !!!col
    • 1

      基礎類別

      class duck {
          duck() {}
          void fly() {}
          void makeSound() {}
          void run() {}
      }
      
    • 1

      衍生類別,遙控鴨子,會飛不會叫

      class duckRC : public duck {
          duckRC() {}
          void fly() { printf("I can fly"); }
          void makeSound() { printf("I cannot make sound!"); }
      }
      
    • 1

      衍生類別,木頭鴨子,不會飛不會叫

      class duckWood : public duck {
          duckWood() {}
          void fly() { printf("I cannot fly"); }
          void makeSound() { printf("I cannot make sound!"); }
      }
      

但是這樣的話如果基礎類別會更改的話,那麼子類別也全部都會受影響,例如,現在希望所有的鴨子都要有一個「跑」的功能,所以我們就在基礎類別裡加一個run()

class duck {
    void fly() {}
    void makeSound() {}
    void run() { printf("I can run"); }
}

結果現在木頭鴨子也能跑了,這不符合我們的設計,所以我們必須回頭更改木頭鴨子的程式,改成

class duckWood : public duck {
    void fly() { printf("I cannot fly"); }
    void makeSound() { printf("I cannot make sound!"); }
    void run() { printf("I cannot run!"); }  // <- 要改!
}

如果我們類別很多那麼就很可能有沒改到的漏網之魚bug就發生了。

封裝變動的部份

比較好的方法一旦類別寫完之後,我們就不要動它了,日後新增的功能也不可以影響到他。我們把「會變動」的部份分離出來,變成各自的類別。 為了簡化我們討論「飛」這個功能就好「飛」只分成「會飛」跟「不會飛」2種類別

  • !!!col
    • 1

      基礎類別IFly

      class IFly {
          void doFly() = 0;
      }
      
    • 1

      衍生類別CanFly

      class CanFly {
          void doFly() { printf("I can fly"); }
      }
      
    • 1

      衍生類別CannotFly

      class CannotFly {
          void doFly() { printf("I cannot fly"); }
      }
      

回到鴨子的基礎類別這邊,基礎類別duck本來是直接擁有fly()這個member function現在我們把他改成他擁有的是IFly

class duck {
    duck() {}
    fly() { fly->doFly() };
    IFly* fly = nullptr;
    ...
}

重寫遙控鴨子這個衍生類別:

class duckRC : public duck {
    duckRC() {
        this->fly = new CanFly();
    }
    ...
}

重寫木頭鴨子,不會飛,所以:

class duckWood : public duck {
    duckWood() {
        this->fly = new CannotFly();
    }
}

現在,不管是遙控鴨子或是木頭鴨子,在被呼叫fly()的時候都是根據它fly的「實際類別」來動作遙控鴨子的fly()呼叫的是CanFlydoFly(),木頭鴨子的fly()呼叫的是CannotFlydoFly()它們的動作完全取決於他們初始化的方式而他們初始化的方式則取決於你的產品定義要是今天你需要不同的飛行方式增加新的fly類別即可不會影響到舊有的。要是鴨子基礎類別增加了新的行為如run那麼就幫鴨子基礎類別的run()加個什麼都不做的預設動作就好,反正舊有的鴨子衍生類別本來就對這個新行為沒反應。要是CanFly的定義改變了,那麼你就是改變了使用CanFly的所有類別,這是你的定義明確的改變了,不是有程式被「額外」的改變了。

  • !!!col
    • 1

      原本直接繼承的方式

      !Pasted image 20220526182952.png
    • 2

      封裝變動的部份

      !Pasted image 20220526183019.png

這樣做的另一個好處是fly的初始化是動態的只要再多一個set() function就可以動態的切換實作也就是說你可以從設定檔來決定你的鴨子要長什麼樣子。

觀察者模式Observer Pattern

有一個會產生變動的主角subject與一堆需要觀察變動的觀察者Observer。觀察者向主角註冊當主角發生變化的時候發後通知給觀察者。 !20220614154819_Observer_Pattern.png

Subject方面attach() 就是註冊觀察者,也可以叫做 register()、add() 之類。 detach() 用來移除用戶,也可以叫做 unregister()、remove() 之類。 notify()則是當發生變化時,用來通知所有觀察者的實作。

觀察者方面必須實作 update() 才能收到通知。

裝飾者模式Decorator Pattern

「裝飾者」通常與「被裝飾者」有同樣的界面,「裝飾者」會取代「被裝飾者」的界面,進而改變「被裝飾者」的行為。 裝飾者模式讓物件可以動態的改變行為,進為符合不同的需求。 以書上的例子來說我們多種飲料每種飲料都可以加上不同的配料。例如有奶茶、綠茶、紅茶3種飲料另外有珍珠、紅豆、綠豆、仙草4種配料我們要如何設計出適合的類別來讓每種飲料都可以隨寄的搭配配料呢

假設這樣寫:

  • !!!col
    • 1

      Ingredient class

      class IngredientBubble { ... };
      class IngredientRedbean { ... };
      class IngredientGreenbean { ... };
      class IngredientFairyGrass { ... };
      
    • 1

      Beverage class

      class Beverage {
          int32_t cost() { return cost; }
          int32_t cost;
      }
      
      class BeverageMilkTea : public Beverage { ... };
      class BeverageGreenTea : public Beverage { ... };
      class BeverageBlackTea : public Beverage {
          IngredientBubble* bubble;
          IngredientRedbean* redbean;
          IngredientGreenbean* greenbean;
          IngredientFairyGrass* fairyGrass;
      }
      

每個飲料的class裡面都將每個配料定義為一個 member如果客人有加配料的話我們就將配料實例化假設奶茶加了珍珠

class BeverageBlackTea {
    void addBubble() {
        if (!bubble) bubble = new IngredientBubble();
    }
};

要算價格的時候:

class BeverageBlackTea {
    int32_t cost() {
        if (bubble) cost += 10;    // 珍珠要加10元
        if (redbean) cost += 5;    // 紅豆要加10元
        if (greenbean) cost += 7;  // 綠豆要加10元
        if (fairyGrass) cost += 9; // 珍珠要加10元
        return cost;
    }
    int32_t cost = 30;  // 奶茶本身10元
};

這樣的問題是每當有一種新配料出現我們就要在奶茶類別裡修改至少2個 functionaddXXX()cost()目前我們有3種飲料所以要修改6個 function更何況如果客人要加2份珍珠怎麼辦這明顯不利程式的維護必須有一種方法讓程式的修改最小讓寫好的程式不用被修改才行。

讓裝飾者模式來改善這個問題。首先讓配料跟飲料有同樣的界面,但是修改一下配料的 constructor當然也要實作 cost(),畢竟每個配料的價格不同:

class IngredientBubble : public Beverage {
    IngredientBubble(Beverage* beverage) {
        this->beverage = beverage;
    }

    int32_t cost() {
        return 10 + this->beverage->cost();  // 珍珠要加10元在加上原本飲料的價錢
    }

    Beverage* beverage;
};

珍珠這個 class 的 constructor 的參數是任何一個 Beverage現在飲料被包在珍珠裡面由珍珠來決定飲料最後的價格是多少。現在我們可以動態的決定飲料的組成假設點一杯紅茶加一些配料

int main() {
    ...

    // 為了讓程式看起來簡單這裡就先不考慮memory leak的問題...>_<
    BeverageBlackTea*     berverge = new BeverageBlackTea();             // 點一杯紅茶
    IngredientBubble*     berverge = new IngredientBubble(berverge);     // 加珍珠
    IngredientRedbean*    berverge = new IngredientRedbean(berverge);    // 加紅豆
    IngredientFairyGrass* berverge = new IngredientFairyGrass(berverge); // 加仙草

    ...

    return berverge->cost();  // 算價錢
}

可以看到第一個變數是飲料,然後被包珍珠裡面,變成珍珠紅茶,再被包到紅豆裡面,變成紅豆珍珠紅茶,再被包到仙草裡面,變成仙草紅豆珍珠紅茶,到這樣的好處是:

  1. 有新的配料就寫新配料的 class
  2. 因為可以動態的組合,原本寫好的飲料 class 就不用在去動它了,愈少修改,愈少 bug
  3. 我們可以動態的組合配料要加2份以上也沒有問題