C#中對(duì)象狀態(tài)模式教程示例
真實(shí)的故事
當(dāng)老胡還是小胡的時(shí)候,跟隨團(tuán)隊(duì)一起開(kāi)發(fā)一款游戲。這款游戲是一款末日生存類游戲,玩家可以
- 收集資源,兩種,一種金子,一種鐵。
- 升級(jí)自身
- 擊殺敵人
- 用資源合成裝備
項(xiàng)目開(kāi)發(fā)的很順利,我那時(shí)得到一個(gè)任務(wù),是為游戲做一個(gè)新手教程,在這個(gè)教程里面,通過(guò)一系列步驟,引導(dǎo)新手玩家熟悉這個(gè)游戲。游戲設(shè)計(jì)給出的教程包含以下步驟
- 收集金子
- 收集鐵
- 擊殺敵人
- 升級(jí)
同時(shí)要求在不用的階段顯示不同的提示以正確引導(dǎo)玩家??紤]合成裝備算是高級(jí)玩家才會(huì)接觸到的功能,所以暫時(shí)不打算放在新手教程里面。
當(dāng)老大把任務(wù)交給我的時(shí)候,我感覺(jué)簡(jiǎn)單爆了,不就寫一個(gè)新手教程么,要求又那么明確,應(yīng)該要不了多少時(shí)間。于是,一個(gè)上午過(guò)后,我交出了如下代碼。
定義枚舉表示教程進(jìn)度
首先用一個(gè)枚舉,表示教程進(jìn)行的不同程度
enum TutorialState { GetGold, GetIron, KillEnemy, LevelUp }
定義角色類
無(wú)需多言,封裝收集到的資源數(shù)、擊殺敵人數(shù)量、角色等級(jí)和一些升級(jí)接口等
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++; } }
定義教程類
定義一個(gè)教程類,包括
- 顯示幫助文字以協(xié)助玩家通過(guò)當(dāng)前教程步驟
- 判斷玩家是否已經(jīng)完成當(dāng)前教程步驟,若是,切換到下一個(gè)步驟直到完成教程
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"); } } }
測(cè)試代碼
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(); //升級(jí) player.LevelUp(); tutorial.ValidateState(); }
運(yùn)行結(jié)果
看起來(lái)一切都好。。編寫的代碼既能夠根據(jù)當(dāng)前步驟顯示不同的提示,還可以成功的根據(jù)玩家的進(jìn)度切換到下一個(gè)步驟。
于是,我自信滿滿的申請(qǐng)了code review,按照我的想法,這段代碼通過(guò)code review應(yīng)該是板上釘釘?shù)氖虑椋l(shuí)知,老大看到代碼,差點(diǎn)沒(méi)背過(guò)氣去。。。稍微平復(fù)了一下心情之后,他給了我?guī)讉€(gè)靈魂拷問(wèn)。
- GameTutorial需要知道各個(gè)步驟的滿足條件和提示,它是不是知道的太多了?這符合迪米特法則嗎?
- 如果我們游戲之后新增一個(gè)教程步驟,指導(dǎo)玩家升級(jí)武器,是不是GameTutorial需要修改?能有辦法規(guī)避這種新增的改動(dòng)嗎?
- 如果我們要修改現(xiàn)在的教程步驟之間的順序關(guān)系,GameTutorial是不是又不能避免要被動(dòng)刀?能有辦法盡量減少這種修改的工作量嗎?
- Switch case 在現(xiàn)有的情況下已經(jīng)如此長(zhǎng),如果我們?cè)偌尤胄碌牟襟E,這個(gè)方法會(huì)變成又臭又長(zhǎng)的裹腳布嗎?
本來(lái)以為如此簡(jiǎn)單的一個(gè)功能,沒(méi)想到還是有那么多彎彎道道,只怪自己還是太年輕啊!最后他悠悠的告訴我,去看看狀態(tài)模式吧,想想這段代碼可以怎么重構(gòu)。
狀態(tài)模式出場(chǎng)
定義
對(duì)象擁有內(nèi)在狀態(tài),當(dāng)內(nèi)在狀態(tài)改變時(shí)允許其改變行為,這個(gè)對(duì)象看起來(lái)像改變了其類
有點(diǎn)意思,看來(lái)我們可以把教程的不同步驟抽象成不同的狀態(tài),然后在各個(gè)狀態(tài)內(nèi)部實(shí)現(xiàn)切換狀態(tài)和顯示幫助文檔的邏輯,這樣做的好處是
- 符合迪米特法則,把各個(gè)步驟所對(duì)應(yīng)的邏輯推遲到子類,教程類就不需要了解每個(gè)步驟的邏輯細(xì)節(jié),同時(shí)隔離了教程類和狀態(tài)類,確保狀態(tài)類的修改不會(huì)影響教程類
- 符合開(kāi)閉原則,如果新添加步驟,我們僅僅需要添加步驟子類并修改相鄰的步驟切換邏輯,教程類無(wú)需任何改動(dòng)
接著我們看看UML,
一目了然,在我們的例子里面,state就是教程子步驟,context就是教程類,內(nèi)部包含教程子步驟并轉(zhuǎn)發(fā)請(qǐng)求給教程子步驟,我們跟著來(lái)重構(gòu)一下代碼吧。
代碼重構(gòu)
創(chuàng)建狀態(tài)基類
第一步我們需要?jiǎng)h除之前的枚舉,取而代之的是一個(gè)抽象類當(dāng)作狀態(tài)基類,即,各個(gè)教程步驟類的基類。注意,每個(gè)子狀態(tài)要自己負(fù)責(zé)狀態(tài)切換,所以我們需要教程類暴露接口以滿足這個(gè)功能。
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è)步驟,在構(gòu)造函數(shù)中給予初始值
- 暴露接口以讓子狀態(tài)能修改當(dāng)前狀態(tài)以完成狀態(tài)切換
- 因?yàn)樾枰訝顟B(tài)能訪問(wèn)玩家當(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)建各個(gè)子狀態(tài)
接著我們創(chuàng)建各個(gè)子狀態(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)容器
這是模式中沒(méi)有提到的知識(shí)點(diǎn),一般來(lái)說(shuō),為了避免大量的子狀態(tài)對(duì)象被創(chuàng)建,我們會(huì)構(gòu)造一個(gè)狀態(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(); } }
測(cè)試代碼部分保持不變,直接運(yùn)行,結(jié)果和原來(lái)一樣,重構(gòu)成功。
結(jié)語(yǔ)
- 這就是狀態(tài)模式和它的使用場(chǎng)景,比較一下重構(gòu)前和重構(gòu)后的代碼,發(fā)現(xiàn)代碼通過(guò)重構(gòu)滿足了開(kāi)閉原則和迪米特法則,相信重構(gòu)后的代碼能通過(guò)code review吧。_
- 不過(guò)狀態(tài)模式雖然好,也有自己的缺點(diǎn),因?yàn)樾枰粋€(gè)子類對(duì)應(yīng)一個(gè)子狀態(tài),那么子狀態(tài)太多的時(shí)候,就會(huì)出現(xiàn)類爆炸的情況。還請(qǐng)大家多注意。
- 作為行為模式之一的狀態(tài)模式,在日常開(kāi)發(fā)中出現(xiàn)的頻率還是挺高的,比如游戲中經(jīng)常用到的狀態(tài)機(jī),就是狀態(tài)模式的一種應(yīng)用場(chǎng)景,大家在平時(shí)工作中保持善于觀察的眼睛,就能學(xué)到更多的東西。
以上就是C#中對(duì)象狀態(tài)模式 教程示例的詳細(xì)內(nèi)容,更多關(guān)于C#對(duì)象狀態(tài)模式 的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解WPF如何在基礎(chǔ)控件上顯示Loading等待動(dòng)畫
這篇文章主要為大家詳細(xì)介紹了WPF如何在基礎(chǔ)控件上顯示Loading等待動(dòng)畫的效果,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,需要的可以參考一下2023-04-04C#數(shù)據(jù)類型轉(zhuǎn)換(顯式轉(zhuǎn)型、隱式轉(zhuǎn)型、強(qiáng)制轉(zhuǎn)型)
本文詳細(xì)講解了C#數(shù)據(jù)類型轉(zhuǎn)換(顯式轉(zhuǎn)型、隱式轉(zhuǎn)型、強(qiáng)制轉(zhuǎn)型),文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01C#實(shí)現(xiàn)文件上傳及文件下載功能實(shí)例代碼
文件上傳文件下載需求在項(xiàng)目中經(jīng)常會(huì)遇到,今天小編給大家分享C#實(shí)現(xiàn)文件上傳及文件下載功能實(shí)例代碼,需要的朋友參考下吧2017-08-08C#開(kāi)發(fā)微信門戶及應(yīng)用(5) 用戶分組信息管理
這篇文章主要為大家詳細(xì)介紹了C#開(kāi)發(fā)微信門戶及應(yīng)用第五篇,用戶分組信息管理,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06C# Winform 讓整個(gè)窗口都可以拖動(dòng)
Windows 的 API 果然強(qiáng)大啊.以前要實(shí)現(xiàn)全窗口拖動(dòng), 要寫鼠標(biāo)按下和抬起事件, 很是麻煩, 偶爾還會(huì)出現(xiàn) BUG2011-05-05