C++設計模式之策略模式(行爲型模式)

學習軟件設計,向OO高手邁進!
設計模式(Design pattern)是軟件開發人員在軟件開發過程中面臨的一般問題的解決方案。
這些解決方案是衆多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的。
是前輩大神們留下的軟件設計的"招式"或是"套路"。

什麼是策略模式

在本文末尾會給出解釋,待耐心看完demo再看定義,相信你會有更深刻的印象

實例講解

背景

假設我們正在開發一款類似植物大戰殭屍的遊戲,產品經理告訴我們說:所有殭屍的必要條件是可以動、可以攻擊、外觀上可以區分就可以了
沒問題,寫一個抽象類 Character,然後讓所有角色(殭屍)繼承這個類就可以了。So easy
在這裏插入圖片描述

Version 1.0

類圖
在這裏插入圖片描述

抽象類

class Character {
public:
    virtual void Move(void) {
        printf("Move\n");
    }

    virtual void Attack(void) {
        printf("Attack\n");
    }

    // 純虛函數, 由子類實現
    virtual void Display(void) = 0;
};

紅頭殭屍

class RedHeadZombie : public Character {
public:
    virtual void Display(void) {
        printf("My head is Red\n");
    }
};

綠頭殭屍

class GreenHeadZombie : public Character {
public:
    virtual void Display(void) {
        printf("My head is Green\n");
    }
};

main函數

int main(int argc, char *argv[])
{
    Character *p_objRedHeadZombie = new RedHeadZombie();
    Character *p_objGreenHeadZombie = new GreenHeadZombie();

    p_objRedHeadZombie->Move();
    p_objRedHeadZombie->Attack();
    p_objRedHeadZombie->Display();

    p_objGreenHeadZombie->Move();
    p_objGreenHeadZombie->Attack();
    p_objGreenHeadZombie->Display();

    return 0;
}

運行結果

Move
Attack
My head is Red
Move
Attack
My head is Green

在這裏插入圖片描述

寫完了,問題解決,正準備看看新聞資訊的時候,產品經理走過來對我說,需要給每個殭屍設置一個前進速度,而且速度要有區別,不能所有殭屍都一樣的。好吧,設置速度嘛,也簡單,我在抽象類裏面添加一個 Speed 的方法,讓紅頭殭屍和綠頭殭屍都分別實現這個方法。也不難,分分種搞定

Version 1.1

類圖
在這裏插入圖片描述

抽象類

class Character {
public:
    ......
    // 純虛函數, 由子類實現
    virtual void Speed(void) = 0;
};

紅頭殭屍

class RedHeadZombie : public Character {
public:
    ......
    virtual void Speed(void) {
        printf("speed is 3.0\n");
    }
};

綠頭殭屍

class GreenHeadZombie : public Character {
public:
    ......
    virtual void Speed(void) {
        printf("speed is 1.0\n");
    }
};

main函數

int main(int argc, char *argv[])
{
    Character *p_objRedHeadZombie = new RedHeadZombie();
    Character *p_objGreenHeadZombie = new GreenHeadZombie();

    p_objRedHeadZombie->Move();
    p_objRedHeadZombie->Attack();
    p_objRedHeadZombie->Display();
    p_objRedHeadZombie->Speed();

    p_objGreenHeadZombie->Move();
    p_objGreenHeadZombie->Attack();
    p_objGreenHeadZombie->Display();
    p_objGreenHeadZombie->Speed();

    return 0;
}

運行結果

Move
Attack
My head is Red
speed is 3.0
Move
Attack
My head is Green
speed is 1.0

在這裏插入圖片描述

好吧,該有的速度也有了,提交代碼,這下滿足你了吧,產品經理這時走過來告訴你,殭屍的攻擊方式不能一樣的,你怎麼讓所有殭屍的攻擊方式都一樣了,這肯定不行的。好像有點道理的樣子,那我就改咯。我重寫所有的 Attack 方法不就行了

Version 1.2

類圖
在這裏插入圖片描述

紅頭殭屍

class RedHeadZombie : public Character {
public:
    ......
    virtual void Attack(void) {
        printf("RedHeadZombie's attack\n");
    }
};

綠頭殭屍

class GreenHeadZombie : public Character {
public:
    ......
    virtual void Attack(void) {
        printf("GreenHeadZombie's attack\n");
    }
};

main函數,不變

運行結果

Move
RedHeadZombie's attack
My head is Red
speed is 3.0
Move
GreenHeadZombie's attack
My head is Green
speed is 1.0

修改完成,攻擊方式也不一樣了
在這裏插入圖片描述

這時候我偷偷瞄了一眼產品經理,發現這貨正在向我走過來…此處略掉一萬字。反正最後結果就是,他覺得需要再添加兩種殭屍(短腿殭屍,無攻擊力殭屍),短腿殭屍速度很慢,但是可以遠程攻擊,無攻擊力殭屍(可能是後勤部隊,用來給其他殭屍運送軍火的)可以移動,但是不能攻擊。還是乖乖寫吧,繼續寫兩個類 ShortLegZombie(短腿),NoAttackZombie(無攻擊力)繼承自 Character

Version 1.3

類圖
在這裏插入圖片描述

短腿殭屍

class ShortLegZombie : public Character {
public:
    virtual void Display(void) {
        printf("I'm ShortLegZombie\n");
    }

    virtual void Speed(void) {
        printf("speed is slow\n");
    }

    virtual void Attack(void) {
        printf("ShortLegZombie's attack\n");
    }
};

無攻擊力殭屍

class NoAttackZombie : public Character {
public:
    virtual void Display(void) {
        printf("I'm NoAttackZombie\n");
    }

    virtual void Speed(void) {
        printf("speed is 5.0\n");
    }

    virtual void Attack(void) {
        // nothing to do
    }
};

main函數

int main(int argc, char *argv[])
{
    Character *p_objRedHeadZombie = new RedHeadZombie();
    Character *p_objGreenHeadZombie = new GreenHeadZombie();
    Character *p_objShortLegZombie = new ShortLegZombie();
    Character *p_objNoAttackZombie = new NoAttackZombie();

    p_objRedHeadZombie->Move();
    p_objRedHeadZombie->Attack();
    p_objRedHeadZombie->Display();
    p_objRedHeadZombie->Speed();

    p_objGreenHeadZombie->Move();
    p_objGreenHeadZombie->Attack();
    p_objGreenHeadZombie->Display();
    p_objGreenHeadZombie->Speed();

    p_objShortLegZombie->Move();
    p_objShortLegZombie->Attack();
    p_objShortLegZombie->Display();
    p_objShortLegZombie->Speed();

    p_objNoAttackZombie->Move();
    p_objNoAttackZombie->Attack();
    p_objNoAttackZombie->Display();
    p_objNoAttackZombie->Speed();

    return 0;
}

運行結果

Move
RedHeadZombie's attack
My head is Red
speed is 3.0
Move
GreenHeadZombie's attack
My head is Green
speed is 1.0
Move
ShortLegZombie's attack
I'm ShortLegZombie
speed is slow
Move
I'm NoAttackZombie
speed is 5.0

完成了,還好,不算太慢。搞定
在這裏插入圖片描述

過一會兒,產品經理又走過來和我說,剛剛開會了,他們最後的決定是,需要再多添加下面幾種殭屍來增加遊戲的可玩性,豐富界面,提高用戶可選性,辛苦你們了,最近加加班咯。好吧,一看,四十幾種!
在這裏插入圖片描述

又添加了幾個殭屍類,先拿給產品經理看一下。產品經理一看,眼睛微眯,皺着眉頭:你這不行呀,你這殭屍怎麼速度都是不變的?你遇到障礙物肯定得減速,沒有障礙物得加速呀,這個要修改。殭屍在起跑線應該都不具有攻擊方式,你這個攻擊方式需要修改下。 聽你這麼一說好像有點道理,當殭屍滿足某個條件時修改下速度和攻擊方式。但是我現在已經創建十幾個殭屍類了,這要修改的工作量有點大呀!而且還有幾十種都沒寫上去呢!

思考改進

在軟件開發上,有什麼是你可以深信不疑的?
不管當初軟件設計得多好,一段時間之後,總是需要成長與改變,否則軟件就會“死亡”

能不能優化一下代碼,讓新建一個殭屍類的時候儘量用幾行代碼就搞定?而且可以隨時修改其行爲
這樣我們重新梳理下思路:我們編寫一個抽象類(Character),讓所有的殭屍都繼承這個類。速度和攻擊方式因爲是可以動態改變的,所以我們將需要動態改變的部分抽離出來。我們分別編寫 ISpeedBehavior 和 IAttackBehavior 兩個接口,將所有的速度和攻擊行爲都抽離出來。速度和攻擊這裏暫時寫三種吧

設計原則:找出應用中可能需要變化的地方,把它們獨立出來,不要和那些不需要變化的代碼混在一起

類圖
在這裏插入圖片描述

我們想要產生一個新的紅頭殭屍實例,並指定特定攻擊方式給它,所以乾脆讓殭屍的行爲可以動態的改變好了。換句話說,我們應該在 Character 類中包含設定行爲的方法,這樣就可以在運行時動態的改變紅頭殭屍的攻擊方式了。我們分別編寫 SetAttackBehavior 和 SetSpeedBehavior 兩個方法,用於設置殭屍的攻擊和速度行爲

類圖
在這裏插入圖片描述

回想一下之前的做法:速度行爲和攻擊行爲來自 Character 抽象類的具體實現,或是繼承某個接口並由子類自行實現而來,這兩種做法都是依賴於“實現”,我們被實現綁得死死的,沒辦法更改行爲(除非寫更多代碼)
在我們的新設計中,Character 的子類將使用接口(ISpeedBehavior 和 IAttackBehavior)所表示的行爲,所以實際的“實現”不會被綁死在 Character 的子類中,換句話說,特定的攻擊行爲和速度行爲的代碼寫在了實現 ISpeedBehavior 和 IAttackBehavior 的類中,即 SuperAttack 和 FastSpeed 等類中

設計原則:針對接口編程,而不是針對實現編程

接下來我們重新整理下代碼

Version 2.0

類圖
在這裏插入圖片描述

IAttackBehavior 接口及其實現類

class IAttackBehavior {
public:
    // 純虛函數, 由子類實現
    virtual void Attack(void) = 0;
};

class NoAttack : public IAttackBehavior {
public:
    virtual void Attack(void) {
        printf("NoAttack\n");
    }
};

class OrdinaryAttack : public IAttackBehavior {
public:
    virtual void Attack(void) {
        printf("OrdinaryAttack\n");
    }
};

class ReinforceAttack : public IAttackBehavior {
public:
    virtual void Attack(void) {
        printf("ReinforceAttack\n");
    }
};

class SuperAttack : public IAttackBehavior {
public:
    virtual void Attack(void) {
        printf("SuperAttack\n");
    }
};

ISpeedBehavior 接口及其實現類

class ISpeedBehavior {
public:
    // 純虛函數, 由子類實現
    virtual void Speed(void) = 0;
};

class SlowSpeed : public ISpeedBehavior {
public:
    virtual void Speed(void) {
        printf("SlowSpeed\n");
    }
};

class NormalSpeed : public ISpeedBehavior {
public:
    virtual void Speed(void) {
        printf("NormalSpeed\n");
    }
};

class FastSpeed : public ISpeedBehavior {
public:
    virtual void Speed(void) {
        printf("FastSpeed\n");
    }
};

抽象類

class Character {
public:
    IAttackBehavior *m_pAttackBehavior;
    ISpeedBehavior *m_pSpeedBehavior;

    virtual void Move(void) {
        printf("Move\n");
    }

    virtual void PerformAttack(void) {
        m_pAttackBehavior->Attack();
    }

    virtual void PerformSpeed(void) {
        m_pSpeedBehavior->Speed();
    }

    // 隨時調用這個方法改變殭屍的攻擊行爲
    virtual void SetAttackBehavior(IAttackBehavior *i_pAttackBehavior) {
        m_pAttackBehavior = i_pAttackBehavior;
    }

    // 隨時調用這個方法改變殭屍的速度行爲
    virtual void SetSpeedBehavior(ISpeedBehavior *i_pSpeedBehavior) {
        m_pSpeedBehavior = i_pSpeedBehavior;
    }

    // 純虛函數, 由子類實現
    virtual void Display(void) = 0;
};

Character 的子類

class RedHeadZombie : public Character {
public:
    RedHeadZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
        m_pAttackBehavior = i_pAttackBehavior;
        m_pSpeedBehavior = i_pSpeedBehavior;
    }

    virtual void Display(void) {
        printf("My head is Red\n");
    }
};

class GreenHeadZombie : public Character {
public:
    GreenHeadZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
        m_pAttackBehavior = i_pAttackBehavior;
        m_pSpeedBehavior = i_pSpeedBehavior;
    }

    virtual void Display(void) {
        printf("My head is Green\n");
    }
};

class ShortLegZombie : public Character {
public:
    ShortLegZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
        m_pAttackBehavior = i_pAttackBehavior;
        m_pSpeedBehavior = i_pSpeedBehavior;
    }

    virtual void Display(void) {
        printf("I'm ShortLegZombie\n");
    }
};

class NoAttackZombie : public Character {
public:
    NoAttackZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
        m_pAttackBehavior = i_pAttackBehavior;
        m_pSpeedBehavior = i_pSpeedBehavior;
    }

    virtual void Display(void) {
        printf("I'm NoAttackZombie\n");
    }
};

main 函數

int main(int argc, char *argv[])
{
    // 殭屍在起跑線應該都不具有攻擊方式, 默認正常速度吧
    IAttackBehavior *p_objNoAttack = new NoAttack();
    ISpeedBehavior *p_objNormalSpeed = new NormalSpeed();

    Character *p_objRedHeadZombie = new RedHeadZombie(p_objNoAttack, p_objNormalSpeed);
    Character *p_objGreenHeadZombie = new GreenHeadZombie(p_objNoAttack, p_objNormalSpeed);
    Character *p_objShortLegZombie = new ShortLegZombie(p_objNoAttack, p_objNormalSpeed);
    Character *p_objNoAttackZombie = new NoAttackZombie(p_objNoAttack, p_objNormalSpeed);

    p_objRedHeadZombie->Move();
    p_objRedHeadZombie->Display();
    p_objRedHeadZombie->PerformAttack();
    p_objRedHeadZombie->PerformSpeed();

    p_objGreenHeadZombie->Move();
    p_objGreenHeadZombie->Display();
    p_objGreenHeadZombie->PerformAttack();
    p_objGreenHeadZombie->PerformSpeed();

    p_objShortLegZombie->Move();
    p_objShortLegZombie->Display();
    p_objShortLegZombie->PerformAttack();
    p_objShortLegZombie->PerformSpeed();

    p_objNoAttackZombie->Move();
    p_objNoAttackZombie->Display();
    p_objNoAttackZombie->PerformAttack();
    p_objNoAttackZombie->PerformSpeed();

    // 動態改變紅頭殭屍的行爲
    p_objRedHeadZombie->SetAttackBehavior(new ReinforceAttack());
    p_objRedHeadZombie->SetSpeedBehavior(new FastSpeed());
    p_objRedHeadZombie->Display();
    p_objRedHeadZombie->PerformAttack();
    p_objRedHeadZombie->PerformSpeed();

    // 動態改變短腿殭屍的行爲
    p_objShortLegZombie->SetAttackBehavior(new SuperAttack());
    p_objShortLegZombie->SetSpeedBehavior(new SlowSpeed());
    p_objShortLegZombie->Display();
    p_objShortLegZombie->PerformAttack();
    p_objShortLegZombie->PerformSpeed();

    return 0;
}

運行結果

Move
My head is Red
NoAttack
NormalSpeed
Move
My head is Green
NoAttack
NormalSpeed
Move
I'm ShortLegZombie
NoAttack
NormalSpeed
Move
I'm NoAttackZombie
NoAttack
NormalSpeed
My head is Red
ReinforceAttack
FastSpeed
I'm ShortLegZombie
SuperAttack
SlowSpeed

這下ok了,隨便你添加角色,隨便你修改速度和攻擊行爲,我都能隨時修改
在這裏插入圖片描述

策略模式定義

現在,我們來說下什麼是策略模式?沒錯,我們上面的實例用的就是策略模式,分別將程序中變化的部分封裝起來,讓它們之間可以相互替換(速度和攻擊行爲)。也就是說策略模式可以讓算法的變化獨立於使用算法的客戶

“有一個”可能比“是一個”更好
“有一個”關係相當有趣:每一個殭屍都有一個 IAttackBehavior 和 ISpeedBehavior,好將攻擊和速度委託給它們代爲處理
當你將兩個類結合起來使用,如同本例一樣,這就是組合(composition)。這種做法和“繼承”不同的地方在於,殭屍的行爲不是繼承而來的,而是和適當的行爲對象“組合”得來的,這是一個很重要的技巧

設計原則:多用組合,少用繼承

策略模式的優缺點

無論哪種模式都有其優缺點,當然我們每次在編寫代碼的時候需要考慮下其利弊
策略模式的優點:

  1. 提供了快速替換繼承關係的辦法:本案例中我們能快速的替換殭屍類的速度,攻擊類型

  2. 減少代碼中的 if else 等判斷語句:我們在更換攻擊狀態的時候不用去判斷,當達到某個條件時直接切換攻擊實現類就可以了。比如上文中的 NoAttack 變 SuperAttack

  3. 實現的選擇:策略模式可以提供不同的實現

策略模式的缺點:

  1. 策略模式會造成很多策略類:上文中的所有速度實現類,攻擊實現類……

  2. 客戶必須知道所有的策略實現類,必須自行知道所有的策略實現類有何不同,並決定使用哪一個。客戶每次新建一個殭屍類的時候必須要知道所有的速度類及攻擊類的不同,此時就會暴露出具體的實現問題。

總結

現在我們回顧下剛開始說的策略模式所用到的三個設計原則:

  1. 封裝變化。我們將變化的速度和攻擊行爲封裝出來,供動態改變調用
  2. 針對接口、超類編程,不針對實現編程。我們用 ISpeedBehavior 和 IAttackBehavior 接口去代表速度和攻擊行爲,每種具體的行爲類實現其中一個接口
  3. 多用組合,少用繼承。組合最大的好處就是具有彈性,給代碼維護和需求修改提供極大的便利。最開始我們使用的就是繼承的方式,在添加大量殭屍類的時候,代碼的維護及修改量將非常大。後面優化過後我們就使用了速度和攻擊類及殭屍類的組合去編寫代碼

其實策略模式在遊戲開發中是經常會使用到的。比如說街機西遊記,有不同的角色:豬八戒,白龍馬,孫悟空。他們都有武器,武器可能被妖怪打掉,也能自己撿起來,還能發不同的招數…
在這裏插入圖片描述

參考資料

https://blog.csdn.net/qq_32175491/article/details/79465496
Head+First設計模式(中文版).pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章