狀態模式-將狀態和行為封裝成對象

公號:碼農充電站pro
主頁://codeshellme.github.io

本篇文章來介紹狀態模式State Design Pattern),狀態模式常用來實現狀態機,狀態機常用在遊戲開發等領域。

1,狀態模式

狀態模式的定義為:允許對象在內部狀態改變時,改變它的行為,對象看起來好像改變了它的類。

狀態模式將狀態和行為封裝成對象,不同的對象有着不同的行為。對象的狀態會因某個行為的發生而改變,對象的狀態一旦改變,那麼對象的行為也會發生改變。

對象的狀態和行為,可以用下面這個圖來解釋。假如一個事物有三種狀態 1,2,3,狀態之間的轉換關係如下:

在這裡插入圖片描述

在上面的狀態轉換圖中,每種狀態對應着不同的行為:

  • 狀態 1:有兩種行為 ab
    • 狀態 1 經過 a 行為可轉換到狀態 2
    • 狀態 1 經過 b 行為可轉換到狀態 3
  • 狀態 2:有兩種行為 cd
    • 狀態 2 經過 c 行為可轉換到狀態 1
    • 狀態 2 經過 d 行為可轉換到狀態 3
  • 狀態 3:有一種行為 e
    • 狀態 3 經過 e 行為可轉換到狀態 1

狀態模式的類圖如下:

在這裡插入圖片描述

State 接口定義了狀態可能擁有的所有行為,每個具體的狀態都實現了這個接口,這樣就使得狀態之間可以互相替換。

每個具體狀態對 State 接口中的每個行為的實現是不一樣的,這就相當於每個具體狀態的行為是不一樣的。

StateMachine 是一個狀態機,它擁有着一個狀態對象,這個狀態對象會不斷的改變。

2,遊戲需求

假設我們要為一款遊戲中的角色編寫狀態轉換的程序,並且遊戲角色有積分:

在這裡插入圖片描述

該遊戲中的角色共有 4 種狀態 A,B,C,D,共有 3 種操作 x,y,z

  • 狀態 A:只能進行 x 操作,轉化到狀態 B
    • 狀態 A 為初始狀態
  • 狀態 B:有兩種操作:
    • x 操作:轉化到狀態 C
    • y 操作:轉化到狀態 D
  • 狀態 C:有兩種操作
    • x 操作:轉化到狀態 D
    • z 操作:轉化到狀態 A
  • 狀態 D:只能進行 z 操作,轉化到狀態 C

積分變化:

  • 操作 x 會使角色增加 100 積分
  • 操作 y 會使角色增加 200 積分
  • 操作 z 會使角色減少 50 積分

3,編寫代碼

下面我們使用狀態模式來編寫角色的狀態轉換程序。

首先根據狀態模式的類圖,我們需要有一個 State 接口,該接口包含角色所有的操作,並且包含一個狀態機的引用。

這裡我將 State 作為一個抽象類,每個操作的默認實現是 do nothing,每個具體狀態可以根據自己的需要進行覆蓋。

代碼如下:

abstract class State {
    protected String stateName;
    protected RoleStateMachine machine;
    
    void x() {
        // do nothing
    }
    
    void y() {
        // do nothing
    }
    
    void z() {
        // do nothing
    }

    // 獲取當前狀態名
    public String getStateName() {
        return stateName;
    }
}

接下來編寫角色狀態機類,代碼中也都寫了注釋:

class RoleStateMachine {
    private State currentState; // 當前狀態
    private int score;          // 積分

    public RoleStateMachine() {
        this.score = 0; // 初始積分為 0
        // 初始狀態為 A
        this.currentState = new StateA(this);
    }

    // 當發生某個操作時需要轉化到相應的狀態
    // 用該方法進行設置
    public void setCurrentState(State state) {
        currentState = state;
    }

    // 獲取當前狀態
    public String getCurrentState() {
        return currentState.getStateName();
    }

    // 獲取積分
    public int getScore() {
        return score;
    }

    // 增加積分
    public void addScore(int score) {
        this.score += score;
    }
    
    // 減少積分
    public void delScore(int score) {
        this.score -= score;
    }

    // 狀態機中也包含狀態中的所有操作
    // 每個操作都委託給當前狀態的相應操作來完成

    public void x() {
        currentState.x();
    }

    public void y() {
        currentState.y();
    }

    public void z() {
        currentState.z();
    }
}

下面編寫 4 個狀態類,每個狀態類都繼承 State 接口,並且每個狀態類中要持有一個狀態機的引用,由構造函數引入:

class StateA extends State {
    public StateA(RoleStateMachine machine) {
        this.machine = machine;
        this.stateName = "StateA";
    }

    public void x() {
        machine.addScore(100);
        machine.setCurrentState(new StateB(machine));
    }
}

class StateB extends State {
    public StateB(RoleStateMachine machine) {
        this.machine = machine;
        this.stateName = "StateB";
    }

    public void x() {
        machine.addScore(100);
        machine.setCurrentState(new StateC(machine));
    }

    public void y() {
        machine.addScore(200);
        machine.setCurrentState(new StateD(machine));
    }
}

class StateC extends State {
    public StateC(RoleStateMachine machine) {
        this.machine = machine;
        this.stateName = "StateC";
    }

    public void x() {
        machine.addScore(100);
        machine.setCurrentState(new StateD(machine));
    }

    public void z() {
        machine.delScore(50);
        machine.setCurrentState(new StateA(machine));
    }
}

class StateD extends State {
    public StateD(RoleStateMachine machine) {
        this.machine = machine;
        this.stateName = "StateD";
    }

    public void z() {
        machine.delScore(50);
        machine.setCurrentState(new StateC(machine));
    }
}

4,測試代碼

下面來測試代碼:

RoleStateMachine role = new RoleStateMachine();

// 初始狀態為 StateA,積分為 0
assert role.getCurrentState().equals("StateA");
assert role.getScore() == 0;

role.y(); // 在狀態 A 進行 y 操作

// 在狀態 A 時,沒有 y 操作
// 所以如果進行 y 操作,狀態和積分都保持不變
assert role.getCurrentState().equals("StateA");
assert role.getScore() == 0;

role.x(); // 在狀態 A 進行 x 操作
assert role.getCurrentState().equals("StateB");
assert role.getScore() == 100;

role.y(); // 在狀態 B,進行 y 操作
assert role.getCurrentState().equals("StateD");
assert role.getScore() == 300;

role.z(); // 在狀態 D,進行 z 操作
assert role.getCurrentState().equals("StateC");
assert role.getScore() == 250;

role.z(); // 在狀態 C,進行 z 操作
assert role.getCurrentState().equals("StateA");
assert role.getScore() == 200;

System.out.println("Test OK.");

注意,使用 Java assert 時,記得用 -ea 參數打開斷言功能。

我將完整的代碼放在了這裡,供大家參考。

5,總結

狀態模式將狀態和行為封裝成對象,不同的狀態有着不同的行為。這種設計使得處理狀態轉換這一類的邏輯變得非常有條理,而且不易出錯。

(本節完。)


推薦閱讀:

命令模式-將請求封裝成對象

適配器模式-讓不兼容的接口得以適配

外觀模式-簡化子系統的複雜性

模板方法模式-封裝一套算法流程

迭代器模式-統一集合的遍歷方式


歡迎關注作者公眾號,獲取更多技術乾貨。

碼農充電站pro