前幾天講了工廠模式,今天要練習的設計模式為策略模式。
情境說明
假設目前要開發一套遊戲,根據料理名稱,調整HP或MP。
釐清業務需求
設計模式中,很多時候會用到抽象或父類別,只有先釐清抽象的需求後,才有辦法理解設計模式能使用在何種場景。照老樣子,阿猩先清點此次範例要完成的事情。
例如
- 使用者輸入玩家名稱
- 建立玩家的物件
- 各種料理對玩家的影響,例如加HP、扣MP等
- 改變玩家HP或MP數值
- 顯示玩家數值結果
依職責建立物件
建立玩家的物件
預設玩家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("================");
耦合的味道
目前已經完成模擬遊戲中,吃料理對玩家的影響。但真實遊戲中,料理絕對部會只有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。
策略模式思考重點
- 上一篇的工廠模式,是產生出不同類型的物件,策略模式是將不同物件傳入特定物件。
- 使用策略模式時,為了能傳入不同的策略Object至特定物件,特定物件使用建構子或參數,應依賴於抽象或業務的共用父類別,其實就是IOC的概念。
- 策略模式的使用時機,是特定物件引用外部物件,外部可以傳入任何物件,只要傳入的物件,有繼承父類別或抽象。實作內容也與特定物件內部無關,而傳入物件的生命週期控制,可另外以DI的方式控制,這也是.NET Core核心開發思維。
- 如果料理要由使用者輸入,可另外寫一隻程式,判斷要產生哪個策略,再將塞入Eating()。