【Design Pattern】策略模式

前幾天講了工廠模式,今天要練習的設計模式為策略模式

 

 

情境說明


假設目前要開發一套遊戲,根據料理名稱,調整HP或MP。


釐清業務需求
設計模式中,很多時候會用到抽象或父類別,只有先釐清抽象的需求後,才有辦法理解設計模式能使用在何種場景。照老樣子,阿猩先清點此次範例要完成的事情。

例如

  1. 使用者輸入玩家名稱
  2. 建立玩家的物件
  3. 各種料理對玩家的影響,例如加HP、扣MP等
  4. 改變玩家HP或MP數值
  5. 顯示玩家數值結果

 

 

依職責建立物件


建立玩家的物件
預設玩家HP=100,MP=50,一開始只需要設定玩家名稱。練習此範例時,要多次的察看玩家狀態,是否真的如預期改變,故阿猩也寫了一個ShowStatus(),可察看玩家目前的狀態,包含HP與MP。

public class Player1
{
    private string _name;
    private int _hp = 100;
    private int _mp = 50;

    public Player1(string name)
    {
        _name = name;
    }

    public void ShowStatus()
    {
       Console.WriteLine($"{_name}的HP: {_hp}");
       Console.WriteLine($"{_name}的MP: {_mp}");
       Console.WriteLine();
    }
}

 

玩家吃料理後對數值的影響
例如吃了香蕉,HP會+10,吃了蘋果,MP+5,萬一不小心吃了大便,HP會只剩1。初階寫法會在Player1中加入程式碼,像是

public class Player1
{
    private string _name;
    private int _hp = 100;
    private int _mp = 50;

    public Player1(string name)
    {
        _name = name;
    }
 
    public void ShowStatus()
    {
       Console.WriteLine($"{_name}的HP: {_hp}");
       Console.WriteLine($"{_name}的MP: {_mp}");
       Console.WriteLine();
    }

    public void Eating(string food)
    {
       Console.WriteLine($"{_name} 吃了 {food}");
       Console.WriteLine();

       if (food.Equals("香蕉")) { _hp += 10; }
       else if (food.Equals("牛奶")) { _mp += 5; }
       else if (food.Equals("大便")) { _hp = 1; }
    }
}

 

此範例流程撰寫
在Program.cs中,控制此次範例的流程,從使用者輸入玩家資訊,及要吃的料理,最後顯示玩家的狀態(圖1)。
 

// 1. 宣告變數
string playerName = string.Empty;


// 第一種寫法 測試player吃各種料理的影響
            
// 2. 玩家1初始化
Console.WriteLine("請輸入玩家名稱:");
playName = Console.ReadLine();
Player1 player1 = new Player1(playerName);
player1.ShowStatus();

// 3. 玩家1吃了大便
player1.Eating("大便");
player1.ShowStatus();

// 4. 玩家1吃了香蕉
player1.Eating("香蕉");
player1.ShowStatus();
Console.WriteLine("================");
圖1 玩家吃料理的狀態

 

 

 

 

 

 

 

 

 

 

 

耦合的味道
目前已經完成模擬遊戲中,吃料理對玩家的影響。但真實遊戲中,料理絕對部會只有3種,只要業主要求對料理作異動,如改料理名稱(Ex: 香蕉 => 大玉香蕉)、新增料理等,勢必就得不停地修改 public void Eating(string food)的內容。IF或Switch Case的程式碼也會不停的變肥,日後將會難以維護

另外,改變HP、MP,是Player1的職責,但改變多少,會受到料理的影響,而料理是由外部使用者傳進來的參數,這裡就可以看出一些耦合的味道了。因此要把職責拆成抽象,再來考慮怎麼做。例如玩家吃東西時,只需要單純的設定_hp及_mp,如何影響,應該寫在料理中。

 

改變玩家狀態時,要如何思考程式架構?
其實如果單純要示範HP或MP改變,更簡單的做法,在外部撰寫程式,判斷完要如何改變玩家狀態,然後再幫Player新增1個改變狀態的Method,例如

 

public void ChangeHP(int hp)
{
    _hp += hp;
    Console.WriteLine($"{_name}的HP: {_hp}"
}

 

但阿猩希望能讓Player吃東西時,依不同料理有不同的表現,就像範例1,吃了香蕉,會在螢幕上印出 阿猩 吃了香蕉。因此找出所有料理的共通點,寫成父類別或抽象。

阿猩使用Interface,定義食物可做2件事,取得中文名稱(GetName),以及改變玩家狀態(ChangeStatus),傳入玩家的狀態後,會回傳更新後的狀態;

public interface IFood
{
   string GetName();
   Tuple<int, int> ChangeStatus(int hp, int mp);
}

 

料理的實作
這裡阿猩有點偷懶,ChangeStatus回傳型別用Tuple。

public class Banana : IFood
{
    public Banana()
    {
    }

    public string GetName()
    {
       return "香蕉";
    }

    public Tuple<int, int> ChangeStatus (int hp, int mp)
    {
        hp += 10;
        return Tuple.Create(hp, mp);
    }

public class Apple: IFood
{
    public Apple()
    {
    }

    public string GetName()
    {
        return "蘋果";
    }

    public Tuple<int, int> ChangeStatus (int hp, int mp)
    {
        mp += 5;
        return Tuple.Create(hp, mp);
    }
}

 

Player依賴於抽象 
新增Player2的物件,跟Player1的差別,在於Eating()的內容,直接注入IFood,使用food.GetName()取得料理名稱,及ChangeStatus改變玩家狀態。

public class Player2
{
    private string _name;
    private int _hp = 100;
    private int _mp = 50;

    public Player2(string name)
    {
        _name = name;
    }

    public void ShowStatus()
    {
        Console.WriteLine($"{_name}的HP: {_hp}");
        Console.WriteLine($"{_name}的MP: {_mp}");
        Console.WriteLine();
    }

    public void Eating(IFood food)
    {
        Console.WriteLine($"{_name} 吃了 {food.GetName()}");
        Console.WriteLine();

        var rs = food.ChangeStatus(_hp, _mp);
        _hp = rs.Item1;
        _mp = rs.Item2;
    }
}

 

傳入料理物件

因Player.Eating()是以IFood作為接口,而new物件又可以用父類別來承接。例如
IFood ifood = new Banana();因此Programe.cs中,可寫成

// 第二種寫法 測試player吃各種料理的影響
           
// 1. 玩家2初始化
Player2 player2 = new Player2(playerName);
player2.ShowStatus();

// 玩家2吃了香蕉
player2.Eating(new Banana()) ;
player2.ShowStatus();

// 玩家2吃了蘋果
player2.Eating(new Apple());
player2.ShowStatus();
Console.WriteLine("================");

當業主新增料理時,只需要新建立料理物件,繼承並實作IFood就好,完全不用動到Player.Eating內部的程式,要用甚麼策略,就直接將該策略物件塞進Eating(),不用撰寫或修改超級難閱讀的IF ELSE。

 

策略模式思考重點


  1. 上一篇的工廠模式,是產生出不同類型的物件,策略模式是將不同物件傳入特定物件。
  2. 使用策略模式時,為了能傳入不同的策略Object至特定物件,特定物件使用建構子或參數,應依賴於抽象或業務的共用父類別,其實就是IOC的概念。
  3. 策略模式的使用時機,是特定物件引用外部物件,外部可以傳入任何物件,只要傳入的物件,有繼承父類別或抽象。實作內容也與特定物件內部無關,而傳入物件的生命週期控制,可另外以DI的方式控制,這也是.NET Core核心開發思維。
  4. 如果料理要由使用者輸入,可另外寫一隻程式,判斷要產生哪個策略,再將塞入Eating()。