[Design Pattern] 策略模式 (Strategy Pattern) 把跑車行為裝箱吧

摘要:[Design Pattern] 策略模式(Strategy Pattern) 把跑車裝箱吧

前言


  最近在閱讀對於設計模式與程式碼重構的相關書籍,為了方便複習順便在此寫下對於設計模式的實作方法說明。

  首先為何要套用設計模式呢? 在OO的設計模式下,撰寫的程式碼是以"降低耦合提高聚合"為最大目標,我們可以透過套用設計模式的方法來趨近這個目標,第一個就先來介紹策略模式吧,使用策略模式主要的目標就是將有相同行為的動作封裝降低類別間的耦合,並且可以因應執行時期的需求去動態切換該使用哪種行為,聽了一定還是很模糊吧? 讓我舉以下範例來說明。

 

範例


  舉例說明,假如公司開發了一款賽車遊戲,遊戲內一定包含各種不同款跑車而每款車都有加速、剎車、噴射的動作,那以類別來劃分時候應該怎麼做呢? 我們可以透過定義一個包含以上行為的父類別 Car 去實作這些行為,再透過子類別的繼承讓所有子類別都可以使用到這些方法。先來看一下類別圖的樣子,如下圖:

程式碼:


public abstract class Car
{
    public void SpeedUp()
    {
        Console.WriteLine("加速中");
    }

    public void SpeedDown()
    {
        Console.WriteLine("減速中");
    }

    public void UseNOS()
    {
        Console.WriteLine("開啟氮氣加速");
    }
}

public class PorscheCar : Car
{
}

public class AmbulanceCar : Car
{
}

  如此就能夠經由繼承讓子類別可以去呼叫父類擁有的方法了,乍看之下這樣的類別應該可以因應目前遊戲的需求,但是日子久了...遊戲就需要增加更多種不同的玩法以保持玩家的新鮮感,所以這時候老闆就要你修改功能!最後在團隊討論完後決定要增加攻擊的行為,而我們就自然地在Car類別加入了Atk()方法,修改類別圖如下:

但這時候問題來了,當我們增加攻擊方法讓跑車能攻擊後卻發現有些不該可攻擊的車種確能攻擊,例如救護車能攻擊好像不太合邏輯?

 

會發生此種情況是因為救護車也繼承Car類別的攻擊方法,這時候如要處理這種情況一般我們可以Override父類別的Atk()方法即可,如下:


public class AmbulanceCar : Car
{
    public override void Atk()
    {
        // 什麼事都不做...
    }
}

  但是這並不是一種最好的解決方法,可以想想如果當不需要攻擊方法的車種有很多款甚至以後也會陸續加入新車種時,是不是每次都要override這個攻擊方法呢?繼續換個想法,那如果使用介面呢? 使用介面定義Car的行為再由有實作該介面的類別去實作這些動作,如下:

  這樣的做法其實也不恰當,因為透過介面每個類別也必須要去實作方法,一樣當加入新的行為時還是要去一一增加實作此介面的類別,當車款類別一多之後將會變得非常麻煩。那到底應該用什麼做法才比較好呢? 這時候就是 策略模式 出場的時機了!

 

於 Head First Design Pattern 此書中策略模式的定義:

  • 定義了演算法家族,個別封裝起來,讓他們之間可以互相替換,此模式讓演算法的變動獨立於使用操作的物件之外

於 Head First Design Pattern 此書中策略模式的原則:

  • 將演算法中可能的變化獨立出來
  • 針對介面寫程式而非針對實踐去編程
  • 多用合成少用繼承

 

接下來基於以上的定義原則去改寫這個遊戲,從以上範例來看我們可以得出目前有以下行為:

  • 加速
  • 減速
  • 使用道具
  • 攻擊

  我們將開始針對這些動作進行封裝,怎麼說呢? 加速可能還有分緩慢加速、快速加速對吧?  減速亦然,使用氮氣加速我則把他歸類成道具的使用行為而不再限定只是單一種類,攻擊的話也可能會使用不同種武器的攻擊方式(拿槍 or 拿刀)。以上種種的行為都有可能會有許多種的變化,我們當然不可能每增加一種變化就去增加一種方法的方式去實作,如這樣搞就會讓系統變的複雜難以維護。

接下來讓我們看看套用了策略模式後將產生什麼樣的類別圖,如下:

  首先說明一下這張類別圖的關係,我將以上所提到的四種行為分別透過介面加以串接。以Atk來說明,PorscheCar與Atk()原本關係是PorscheCar類別去呼叫父類別或自行實作該Atk()方法,也就是ProscheCar類別必須需要知道我的Atk方法是從哪兒來要些做什麼。但是經過修改後透過 IAtkMode介面 將PorscheCar與GunMode兩者切開了,如此ProscheCar實際上並不用管我到底要用什麼樣的攻擊方式,而是接收由外部傳入攻擊方式物件後再透過Atk()的呼叫去攻擊,這下就把PorscheCar與GunMode攻擊方式的相依分離了,進而降低了PorscheCar類別與其他類別的耦合度。

痾......

PorscheCar類別修改後宣告全域的四種行為介面之後用來存放經由建構子傳入的行為物件:


public class PorscheCar : Car
{
    private ISpeedUpSet _speedUp;
    private ISpeedDownSet _speedDown;
    private IItemSet _item;
    private IAtkMode _atkMode;
}

PorscheCar類別建構子將接受由外部傳入的行為物件,透過使用介面型別接收


public PorscheCar(ISpeedUpSet speedUp, ISpeedDownSet speedDown, IItemSet item, IAtkMode atkMode)
{
    this._speedUp = speedUp;
    this._speedDown = speedDown;
    this._item = item;
    this._atkMode = atkMode;
}

實際的執行交由傳入的介面方法去執行


public void SpeedUp()
{
    this._speedUp.SpeedUp();
}

public void SpeedDown()
{
    this._speedDown.SpeedDown();
}

public void UseItem()
{
    this._item.UseItem();
}

public void Atk()
{
    this._atkMode.Atk();
}

  如此即可達成不用管這些方法到底要做什麼,行為的實作由實作該行為介面的類別去做而PorscheCar只需要知道攻擊就是Atk()管你怎麼實作方法的內容,這樣就切開兩個類別的相依了

最後實際兩種方式使用上的差異就如同以下程式碼:


static void Main(string[] args)
{
    D1BeforeClass.PorscheCar bCar = new D1BeforeClass.PorscheCar();
    bCar.SpeedUp();
    bCar.SpeedDown();
    bCar.UseNOS();
    bCar.Atk();

    Console.WriteLine("----套用策略模式後----");

    D1AfterClass.HighSpeed highSpeed = new D1AfterClass.HighSpeed();
    D1AfterClass.LowSpeed lowSpeed = new D1AfterClass.LowSpeed();
    D1AfterClass.NOS nos = new D1AfterClass.NOS();
    D1AfterClass.GunMode weapon = new D1AfterClass.GunMode();

    D1AfterClass.PorscheCar aCar = new D1AfterClass.PorscheCar(highSpeed, lowSpeed, nos, weapon);
    aCar.SpeedUp();
    aCar.SpeedDown();
    aCar.UseItem();
    aCar.Atk();

    Console.Read();
}

  在使用策略模式的情況下,如果需要更改移動速度與攻擊的攻擊方式,只需要產生新的速度類別與攻擊方式類別後再將傳入參數替換,如此就能夠動態的改名物件行為卻不用去更改原本主體的程式碼嘍,如下:

 

增加


public class SuperHighSpeed : ISpeedUpSet
{
    public void SpeedUp()
    {
        Console.WriteLine("超級高速的移動中");
    }
}

修改


//D1AfterClass.HighSpeed highSpeed = new D1AfterClass.HighSpeed();
D1AfterClass.SuperHighSpeed highSpeed = new D1AfterClass.SuperHighSpeed(); // 替換新物件
D1AfterClass.LowSpeed lowSpeed = new D1AfterClass.LowSpeed();
D1AfterClass.NOS nos = new D1AfterClass.NOS();
//D1AfterClass.GunMode weapon = new D1AfterClass.GunMode();
D1AfterClass.KnifeMode weapon = new D1AfterClass.KnifeMode();    // 替換新物件

 

以上就是第一篇策略模式的說明,在下第一次嘗試這種主題,如有錯誤的地方請包涵指教,謝謝。

 

範例下載


DP_01.rar

 

參考資料


Head First Design Pattern 深入淺出設計模式

Astah 繪UML軟體




以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)