C#中對象狀態(tài)模式教程示例
真實的故事
當(dāng)老胡還是小胡的時候,跟隨團(tuán)隊一起開發(fā)一款游戲。這款游戲是一款末日生存類游戲,玩家可以
- 收集資源,兩種,一種金子,一種鐵。
- 升級自身
- 擊殺敵人
- 用資源合成裝備
項目開發(fā)的很順利,我那時得到一個任務(wù),是為游戲做一個新手教程,在這個教程里面,通過一系列步驟,引導(dǎo)新手玩家熟悉這個游戲。游戲設(shè)計給出的教程包含以下步驟
- 收集金子
- 收集鐵
- 擊殺敵人
- 升級
同時要求在不用的階段顯示不同的提示以正確引導(dǎo)玩家??紤]合成裝備算是高級玩家才會接觸到的功能,所以暫時不打算放在新手教程里面。
當(dāng)老大把任務(wù)交給我的時候,我感覺簡單爆了,不就寫一個新手教程么,要求又那么明確,應(yīng)該要不了多少時間。于是,一個上午過后,我交出了如下代碼。
定義枚舉表示教程進(jìn)度
首先用一個枚舉,表示教程進(jìn)行的不同程度
enum TutorialState
{
GetGold,
GetIron,
KillEnemy,
LevelUp
}
定義角色類
無需多言,封裝收集到的資源數(shù)、擊殺敵人數(shù)量、角色等級和一些升級接口等
class Player
{
private int ironNum;
private int goldNum;
private int enemyKilled;
private int level;
public int IronNum => ironNum;
public int GoldNum => goldNum;
public int EnemyKilled => enemyKilled;
public int Level => level;
public void CollectIron(int num)
{
ironNum += num;
}
public void CollectGold(int num)
{
goldNum += num;
}
public void KillEnemy()
{
enemyKilled++;
}
public void LevelUp()
{
level++;
}
}
定義教程類
定義一個教程類,包括
- 顯示幫助文字以協(xié)助玩家通過當(dāng)前教程步驟
- 判斷玩家是否已經(jīng)完成當(dāng)前教程步驟,若是,切換到下一個步驟直到完成教程
class GameTutorial
{
private TutorialState currentState;
private Player player;
public GameTutorial(Player player)
{
this.player = player;
}
public void ShowHelpDescription()
{
switch (currentState)
{
case TutorialState.GetGold:
Console.WriteLine("Please follow instruction to get gold");
break;
case TutorialState.GetIron:
Console.WriteLine("Please follow instruction to get Iron");
break;
case TutorialState.KillEnemy:
Console.WriteLine("Please follow instruction to kill enemy");
break;
case TutorialState.LevelUp:
Console.WriteLine("Please follow instruction to Up your level");
break;
default:
throw new Exception("Not Support");
}
}
public void ValidateState()
{
switch (currentState)
{
case TutorialState.GetGold:
{
if (player.GoldNum > 0)
{
Console.WriteLine("Congratulations, you finished Gold Collect Phase");
currentState = TutorialState.GetIron;
}
else
{
Console.WriteLine("You need to collect gold");
}
break;
}
case TutorialState.GetIron:
{
if (player.IronNum > 0)
{
Console.WriteLine("Congratulations, you finished Iron Collect Phase");
currentState = TutorialState.KillEnemy;
}
else
{
Console.WriteLine("You need to collect Iron");
}
break;
}
case TutorialState.KillEnemy:
{
if (player.EnemyKilled > 0)
{
Console.WriteLine("Congratulations, you finished Enemy Kill Phase");
currentState = TutorialState.LevelUp;
}
else
{
Console.WriteLine("You need to kill enemy");
}
break;
}
case TutorialState.LevelUp:
{
if (player.Level > 0)
{
Console.WriteLine("Congratulations, you finished the whole tutorial");
currentState = TutorialState.LevelUp;
}
else
{
Console.WriteLine("You need to level up");
}
break;
}
default:
throw new Exception("Not Support");
}
}
}
測試代碼
static void Main(string[] args)
{
Player player = new Player();
GameTutorial tutorial = new GameTutorial(player);
tutorial.ShowHelpDescription();
tutorial.ValidateState();
//收集黃金
player.CollectGold(1);
tutorial.ValidateState();
tutorial.ShowHelpDescription();
//收集木頭
player.CollectIron(1);
tutorial.ValidateState();
tutorial.ShowHelpDescription();
//殺敵
player.KillEnemy();
tutorial.ValidateState();
tutorial.ShowHelpDescription();
//升級
player.LevelUp();
tutorial.ValidateState();
}
運行結(jié)果

看起來一切都好。。編寫的代碼既能夠根據(jù)當(dāng)前步驟顯示不同的提示,還可以成功的根據(jù)玩家的進(jìn)度切換到下一個步驟。
于是,我自信滿滿的申請了code review,按照我的想法,這段代碼通過code review應(yīng)該是板上釘釘?shù)氖虑?,誰知,老大看到代碼,差點沒背過氣去。。。稍微平復(fù)了一下心情之后,他給了我?guī)讉€靈魂拷問。
- GameTutorial需要知道各個步驟的滿足條件和提示,它是不是知道的太多了?這符合迪米特法則嗎?
- 如果我們游戲之后新增一個教程步驟,指導(dǎo)玩家升級武器,是不是GameTutorial需要修改?能有辦法規(guī)避這種新增的改動嗎?
- 如果我們要修改現(xiàn)在的教程步驟之間的順序關(guān)系,GameTutorial是不是又不能避免要被動刀?能有辦法盡量減少這種修改的工作量嗎?
- Switch case 在現(xiàn)有的情況下已經(jīng)如此長,如果我們再加入新的步驟,這個方法會變成又臭又長的裹腳布嗎?
本來以為如此簡單的一個功能,沒想到還是有那么多彎彎道道,只怪自己還是太年輕??!最后他悠悠的告訴我,去看看狀態(tài)模式吧,想想這段代碼可以怎么重構(gòu)。
狀態(tài)模式出場
定義
對象擁有內(nèi)在狀態(tài),當(dāng)內(nèi)在狀態(tài)改變時允許其改變行為,這個對象看起來像改變了其類
有點意思,看來我們可以把教程的不同步驟抽象成不同的狀態(tài),然后在各個狀態(tài)內(nèi)部實現(xiàn)切換狀態(tài)和顯示幫助文檔的邏輯,這樣做的好處是
- 符合迪米特法則,把各個步驟所對應(yīng)的邏輯推遲到子類,教程類就不需要了解每個步驟的邏輯細(xì)節(jié),同時隔離了教程類和狀態(tài)類,確保狀態(tài)類的修改不會影響教程類
- 符合開閉原則,如果新添加步驟,我們僅僅需要添加步驟子類并修改相鄰的步驟切換邏輯,教程類無需任何改動
接著我們看看UML,

一目了然,在我們的例子里面,state就是教程子步驟,context就是教程類,內(nèi)部包含教程子步驟并轉(zhuǎn)發(fā)請求給教程子步驟,我們跟著來重構(gòu)一下代碼吧。
代碼重構(gòu)
創(chuàng)建狀態(tài)基類
第一步我們需要刪除之前的枚舉,取而代之的是一個抽象類當(dāng)作狀態(tài)基類,即,各個教程步驟類的基類。注意,每個子狀態(tài)要自己負(fù)責(zé)狀態(tài)切換,所以我們需要教程類暴露接口以滿足這個功能。
abstract class TutorialState
{
public abstract void ShowHelpDescription();
public abstract void Validate(GameTutorial tutorial);
}
重構(gòu)教程類
重構(gòu)教程類體現(xiàn)在以下方面
- 添加內(nèi)部狀態(tài)表面當(dāng)前處于哪個步驟,在構(gòu)造函數(shù)中給予初始值
- 暴露接口以讓子狀態(tài)能修改當(dāng)前狀態(tài)以完成狀態(tài)切換
- 因為需要子狀態(tài)能訪問玩家當(dāng)前數(shù)據(jù)以判斷是否能切換狀態(tài),需要新加接口以避免方法鏈
- 修改ShowHelpDescription和ValidateState的邏輯,直接轉(zhuǎn)發(fā)方法調(diào)用至當(dāng)前狀態(tài)
class GameTutorial
{
private TutorialState currentState;
private Player player;
public int PlayerIronNum => player.IronNum;
public int PlayerLevel => player.Level;
public int PlayerGoldNum => player.GoldNum;
public int PlayerEnemyKilled => player.EnemyKilled;
public void SetState(TutorialState state)
{
currentState = state;
}
public GameTutorial(Player player)
{
this.player = player;
currentState = TutorialStateContext.GetGold;
}
public void ShowHelpDescription()
{
currentState.ShowHelpDescription();
}
public void ValidateState()
{
currentState.Validate(this);
}
}
創(chuàng)建各個子狀態(tài)
接著我們創(chuàng)建各個子狀態(tài)代表不同的教程步驟
class TutorialSateGetGold : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to get gold");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerGoldNum > 0)
{
Console.WriteLine("Congratulations, you finished Gold Collect Phase");
tutorial.SetState(TutorialStateContext.GetIron);
}
else
{
Console.WriteLine("You need to collect gold");
}
}
}
class TutorialStateGetIron : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to get Iron");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerIronNum > 0)
{
Console.WriteLine("Congratulations, you finished Iron Collect Phase");
tutorial.SetState(TutorialStateContext.KillEnemy);
}
else
{
Console.WriteLine("You need to collect iron");
}
}
}
class TutorialStateKillEnemy : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to kill enemy");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerEnemyKilled > 0)
{
Console.WriteLine("Congratulations, you finished enemy kill Phase");
tutorial.SetState(TutorialStateContext.LevelUp);
}
else
{
Console.WriteLine("You need to collect kill enemy");
}
}
}
class TutorialStateLevelUp : TutorialState
{
public override void ShowHelpDescription()
{
Console.WriteLine("Please follow instruction to level up");
}
public override void Validate(GameTutorial tutorial)
{
if (tutorial.PlayerLevel > 0)
{
Console.WriteLine("Congratulations, you finished the whole tutorial");
}
}
}
添加狀態(tài)容器
這是模式中沒有提到的知識點,一般來說,為了避免大量的子狀態(tài)對象被創(chuàng)建,我們會構(gòu)造一個狀態(tài)容器,以靜態(tài)變量的方式初始化需要使用的子狀態(tài)。
static class TutorialStateContext
{
public static TutorialState GetGold;
public static TutorialState GetIron;
public static TutorialState KillEnemy;
public static TutorialState LevelUp;
static TutorialStateContext()
{
GetGold = new TutorialSateGetGold();
GetIron = new TutorialStateGetIron();
KillEnemy = new TutorialStateKillEnemy();
LevelUp = new TutorialStateLevelUp();
}
}
測試代碼部分保持不變,直接運行,結(jié)果和原來一樣,重構(gòu)成功。
結(jié)語
- 這就是狀態(tài)模式和它的使用場景,比較一下重構(gòu)前和重構(gòu)后的代碼,發(fā)現(xiàn)代碼通過重構(gòu)滿足了開閉原則和迪米特法則,相信重構(gòu)后的代碼能通過code review吧。_
- 不過狀態(tài)模式雖然好,也有自己的缺點,因為需要一個子類對應(yīng)一個子狀態(tài),那么子狀態(tài)太多的時候,就會出現(xiàn)類爆炸的情況。還請大家多注意。
- 作為行為模式之一的狀態(tài)模式,在日常開發(fā)中出現(xiàn)的頻率還是挺高的,比如游戲中經(jīng)常用到的狀態(tài)機(jī),就是狀態(tài)模式的一種應(yīng)用場景,大家在平時工作中保持善于觀察的眼睛,就能學(xué)到更多的東西。
以上就是C#中對象狀態(tài)模式 教程示例的詳細(xì)內(nèi)容,更多關(guān)于C#對象狀態(tài)模式 的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
DevExpress實現(xiàn)TreeList按條件隱藏節(jié)點CheckBox的方法
這篇文章主要介紹了DevExpress實現(xiàn)TreeList按條件隱藏節(jié)點CheckBox的方法,需要的朋友可以參考下2014-08-08

