讓對象擁有狀態——C#中的狀態模式

你們好,老胡又在博客和你們見面了,在聊今天的主角以前,老胡先給你們講一個之前發生的故事。
 ide

真實的故事

當老胡仍是小胡的時候,跟隨團隊一塊兒開發一款遊戲。這款遊戲是一款末日生存類遊戲,玩家能夠函數

  • 收集資源,兩種,一種金子,一種鐵。
  • 升級自身
  • 擊殺敵人
  • 用資源合成裝備

項目開發的很順利,我那時獲得一個任務,是爲遊戲作一個新手教程,在這個教程裏面,經過一系列步驟,引導新手玩家熟悉這個遊戲。遊戲設計給出的教程包含如下步驟測試

  • 收集金子
  • 收集鐵
  • 擊殺敵人
  • 升級

同時要求在不用的階段顯示不一樣的提示以正確引導玩家。
考慮合成裝備算是高級玩家纔會接觸到的功能,因此暫時不打算放在新手教程裏面。this

當老大把任務交給個人時候,我感受簡單爆了,不就寫一個新手教程麼,要求又那麼明確,應該要不了多少時間。因而,一個上午事後,我交出了以下代碼。
 設計

個人代碼
定義枚舉表示教程進度

首先用一個枚舉,表示教程進行的不一樣程度code

enum TutorialState
{
    GetGold,
    GetIron,
    KillEnemy,
    LevelUp
}

 

定義角色類

無需多言,封裝收集到的資源數、擊殺敵人數量、角色等級和一些升級接口等對象

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++;
    }
}

 

定義教程類

定義一個教程類,包括blog

  • 顯示幫助文字以協助玩家經過當前教程步驟
  • 判斷玩家是否已經完成當前教程步驟,如果,切換到下一個步驟直到完成教程
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();
}

運行結果

看起來一切都好。。編寫的代碼既可以根據當前步驟顯示不一樣的提示,還能夠成功的根據玩家的進度切換到下一個步驟。教程

因而,我自信滿滿的申請了code review,按照個人想法,這段代碼經過code review應該是板上釘釘的事情,誰知,老大看到代碼,差點沒背過氣去。。。稍微平復了一下心情以後,他給了我幾個靈魂拷問。接口

  • GameTutorial須要知道各個步驟的知足條件和提示,它是否是知道的太多了?這符合迪米特法則嗎?
  • 若是咱們遊戲以後新增一個教程步驟,指導玩家升級武器,是否是GameTutorial須要修改?能有辦法規避這種新增的改動嗎?
  • 若是咱們要修改如今的教程步驟之間的順序關係,GameTutorial是否是又不能避免要被動刀?能有辦法儘可能減小這種修改的工做量嗎?
  • Switch case 在現有的狀況下已經如此長,若是咱們再加入新的步驟,這個方法會變成又臭又長的裹腳布嗎?

當時個人表情是這樣的

原本覺得如此簡單的一個功能,沒想到仍是有那麼多彎彎道道,只怪本身仍是太年輕啊!最後他悠悠的告訴我,去看看狀態模式吧,想一想這段代碼能夠怎麼重構。
 

狀態模式出場

定義

對象擁有內在狀態,當內在狀態改變時容許其改變行爲,這個對象看起來像改變了其類

有點意思,看來咱們能夠把教程的不一樣步驟抽象成不一樣的狀態,而後在各個狀態內部實現切換狀態和顯示幫助文檔的邏輯,這樣作的好處是

  • 符合迪米特法則,把各個步驟所對應的邏輯推遲到子類,教程類就不須要了解每一個步驟的邏輯細節,同時隔離了教程類和狀態類,確保狀態類的修改不會影響教程類
  • 符合開閉原則,若是新添加步驟,咱們僅僅須要添加步驟子類並修改相鄰的步驟切換邏輯,教程類無需任何改動

接着咱們看看UML,

一目瞭然,在咱們的例子裏面,state就是教程子步驟,context就是教程類,內部包含教程子步驟並轉發請求給教程子步驟,咱們跟着來重構一下代碼吧。
 

代碼重構
建立狀態基類

第一步咱們須要刪除以前的枚舉,取而代之的是一個抽象類看成狀態基類,即,各個教程步驟類的基類。注意,每一個子狀態要本身負責狀態切換,因此咱們須要教程類暴露接口以知足這個功能。

abstract class TutorialState
{
    public abstract void ShowHelpDescription();
    public abstract void Validate(GameTutorial tutorial);
}

 

重構教程類

重構教程類體如今如下方面

  • 添加內部狀態表面當前處於哪一個步驟,在構造函數中給予初始值
  • 暴露接口以讓子狀態能修改當前狀態以完成狀態切換
  • 由於須要子狀態能訪問玩家當前數據以判斷是否能切換狀態,須要新加接口以免方法鏈
  • 修改ShowHelpDescriptionValidateState的邏輯,直接轉發方法調用至當前狀態
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);
    }
}

 

建立各個子狀態

接着咱們建立各個子狀態表明不一樣的教程步驟

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");
        }
    }
}

 

添加狀態容器

這是模式中沒有提到的知識點,通常來講,爲了不大量的子狀態對象被建立,咱們會構造一個狀態容器,以靜態變量的方式初始化須要使用的子狀態。

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();
    }
}

 

測試代碼

測試代碼部分保持不變,直接運行,結果和原來同樣,重構成功。

 

結語

這就是狀態模式和它的使用場景,比較一下重構前和重構後的代碼,發現代碼經過重構知足了開閉原則和迪米特法則,相信重構後的代碼能經過code review吧。_ 不過狀態模式雖然好,也有本身的缺點,由於須要一個子類對應一個子狀態,那麼子狀態太多的時候,就會出現類爆炸的狀況。還請你們多注意。 做爲行爲模式之一的狀態模式,在平常開發中出現的頻率仍是挺高的,好比遊戲中常常用到的狀態機,就是狀態模式的一種應用場景,你們在平時工做中保持善於觀察的眼睛,就能學到更多的東西。 今天就講到這裏吧,謝謝你們的閱讀,下次見。

相關文章
相關標籤/搜索