文章目錄
- 什么是有限狀態機?
- 實作方法一:分支實作法
- 實作方法二:查表法
- 實作方法三:狀態模式
什么是有限狀態機?
有限狀態機,英文翻譯是Finite State Machine,縮寫為FSM,簡稱為狀態機,狀態機有3個組成部分:狀態(State)、事件(Event)、動作(Action),其中,事件也稱為轉移條件(Transition Condition),事件觸發狀態的轉移及動作的執行,不過,動作不是必須的,也可能只轉移狀態,不執行任何動作,
“超級馬里奧”游戲不知道你玩過沒有?在游戲中,馬里奧可以變身為多種形態,比如小馬里奧(Small Mario)、超級馬里奧(Super Mario)、火焰馬里奧(Fire Mario)、斗篷馬里奧(Cape Mario)等等,在不同的游戲情節下,各個形態會互相轉化,并相應的增級訓分,比如,初始形態是小馬里奧,吃了蘑菇之后就會變成超級馬里奧,并且增加100積分,
實際上,馬里奧形態的轉變就是一個狀態機,其中,馬里奧的不同形態就是狀態機中的“狀態”,游戲情節(比如吃了蘑菇)就是狀態機中的“事件”,加級訓分就是狀態機中的“動作”,比如,吃蘑菇這個事件,會觸發狀態的轉移:從小馬里奧轉移到超級馬里奧,以及觸發動作的執行(增加100積分),
為了方便接下來的講解,我對游戲背景做了簡化,只保留了部分狀態和事件,簡化之后的狀態轉移如下圖所示:

實作方法一:分支實作法
package statepattern;
public class MarioStateMachine {
private int score;
private State currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
//TODO
this.score += 100;
if (State.SMALL.equals(this.currentState)) {
this.currentState = State.SUPER;
}
}
public void obtainCape() {
//TODO
this.score += 200;
if (State.SMALL.equals(this.currentState) || State.SUPER.equals(this.currentState)) {
this.currentState = State.CAPE;
} else {
System.out.println("此時狀態不可改變!!!");
}
}
public void obtainFireFlower() {
//TODO
this.score += 300;
if (State.SMALL.equals(this.currentState) || State.SUPER.equals(this.currentState)) {
this.currentState = State.FIRE;
} else {
System.out.println("此時狀態不可改變!!!");
}
}
public void meetMonster() {
//TODO
if (State.FIRE.equals(this.currentState)) {
this.score -= 300;
this.currentState = State.SMALL;
} else if (State.SUPER.equals(this.currentState)) {
this.score -= 100;
this.currentState = State.SMALL;
} else if (State.CAPE.equals(this.currentState)) {
this.score -= 200;
this.currentState = State.SMALL;
} else {
System.out.println("此時狀態不可改變!!!");
}
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState;
}
}
實作方法二:查表法
實際上,上面這種實作方法有點類似hard code,對于復雜的狀態機來說不適用,而狀態機的第二種實作方式查表法,就更加合適了,接下來,我們就一塊兒來看下,如何利用查表法來補全骨架代碼,
實際上,除了用狀態轉移圖來表示之外,狀態機還可以用二維表來表示,如下所示,在這個二維表中,第一維表示當前狀態,第二維表示事件,值表示當前狀態經過事件之后,轉移到的新狀態及其執行的動作,

public enum Event {
GOT_MUSHROOM(0),
GOT_CAPE(1),
GOT_FIRE(2),
MET_MONSTER(3);
private int value;
private Event(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
public class MarioStateMachine {
private int score;
private State currentState;
private static final State[][] transitionTable = {
{SUPER, CAPE, FIRE, SMALL},
{SUPER, CAPE, FIRE, SMALL},
{CAPE, CAPE, CAPE, SMALL},
{FIRE, FIRE, FIRE, SMALL}
};
private static final int[][] actionTable = {
{+100, +200, +300, +0},
{+0, +200, +300, -100},
{+0, +0, +0, -200},
{+0, +0, +0, -300}
};
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
executeEvent(Event.GOT_MUSHROOM);
}
public void obtainCape() {
executeEvent(Event.GOT_CAPE);
}
public void obtainFireFlower() {
executeEvent(Event.GOT_FIRE);
}
public void meetMonster() {
executeEvent(Event.MET_MONSTER);
}
private void executeEvent(Event event) {
int stateValue = currentState.getValue();
int eventValue = event.getValue();
this.currentState = transitionTable[stateValue][eventValue];
this.score += actionTable[stateValue][eventValue];
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState;
}
}
實作方法三:狀態模式
但是在查表法的代碼實作中,事件觸發的動作只是簡單的積分加減,所以,我們用一個int型別的二維陣列actionTable就能表示,二維陣列中的值表示積分的加減值,但是,如果要執行的動作并非這么簡單,而是一系列復雜的邏輯操作(比如加級訓分、寫資料庫,還有可能發送訊息通知等等),我們就沒法用如此簡單的二維陣列來表示了,這也就是說,查表法的實作方式有一定局限性,
雖然分支邏輯的實作方式不存在這個問題,但它又存在前面講到的其他問題,比如分支判斷邏輯較多,導致代碼可讀性和可維護性不好等,實際上,針對分支邏輯法存在的問題,我們可以使用狀態模式來解決,
狀態模式通過將事件觸發的狀態轉移和動作執行,拆分到不同的狀態類中,來避免分支判斷邏輯,我們還是結合代碼來理解這句話,
利用狀態模式,我們來補全MarioStateMachine類,補全后的代碼如下所示,
其中,IMario是狀態的介面,定義了所有的事件,SmallMario、SuperMario、CapeMario、FireMario是IMario介面的實作類,分別對應狀態機中的4個狀態,原來所有的狀態轉移和動作執行的代碼邏輯,都集中在MarioStateMachine類中,現在,這些代碼邏輯被分散到了這4個狀態類中,
public interface IMario { //所有狀態類的介面
State getName();
//以下是定義的事件
void obtainMushRoom();
void obtainCape();
void obtainFireFlower();
void meetMonster();
}
public class SmallMario implements IMario {
private MarioStateMachine stateMachine;
public SmallMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.SMALL;
}
@Override
public void obtainMushRoom() {
stateMachine.setCurrentState(new SuperMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 100);
}
@Override
public void obtainCape() {
stateMachine.setCurrentState(new CapeMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower() {
stateMachine.setCurrentState(new FireMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster() {
// do nothing...
}
}
public class SuperMario implements IMario {
private MarioStateMachine stateMachine;
public SuperMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.SUPER;
}
@Override
public void obtainMushRoom() {
// do nothing...
}
@Override
public void obtainCape() {
stateMachine.setCurrentState(new CapeMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower() {
stateMachine.setCurrentState(new FireMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster() {
stateMachine.setCurrentState(new SmallMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() - 100);
}
}
// 省略CapeMario、FireMario類...
public class MarioStateMachine {
private int score;
private IMario currentState; // 不再使用列舉來表示狀態
public MarioStateMachine() {
this.score = 0;
this.currentState = new SmallMario(this);
}
public void obtainMushRoom() {
this.currentState.obtainMushRoom();
}
public void obtainCape() {
this.currentState.obtainCape();
}
public void obtainFireFlower() {
this.currentState.obtainFireFlower();
}
public void meetMonster() {
this.currentState.meetMonster();
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState.getName();
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
}
實際上,上面的代碼還可以繼續優化,我們可以將狀態類設計成單例,畢竟狀態類中不包含任何成員變數,但是,當將狀態類設計成單例之后,我們就無法通過建構式來傳遞MarioStateMachine了,而狀態類又要依賴MarioStateMachine,那該如何解決這個問題呢?
public interface IMario {
State getName();
void obtainMushRoom(MarioStateMachine stateMachine);
void obtainCape(MarioStateMachine stateMachine);
void obtainFireFlower(MarioStateMachine stateMachine);
void meetMonster(MarioStateMachine stateMachine);
}
public class SmallMario implements IMario {
private static final SmallMario instance = new SmallMario();
private SmallMario() {}
public static SmallMario getInstance() {
return instance;
}
@Override
public State getName() {
return State.SMALL;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(SuperMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 100);
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(CapeMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(FireMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
// do nothing...
}
}
// 省略SuperMario、CapeMario、FireMario類...
public class MarioStateMachine {
private int score;
private IMario currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = SmallMario.getInstance();
}
public void obtainMushRoom() {
this.currentState.obtainMushRoom(this);
}
public void obtainCape() {
this.currentState.obtainCape(this);
}
public void obtainFireFlower() {
this.currentState.obtainFireFlower(this);
}
public void meetMonster() {
this.currentState.meetMonster(this);
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState.getName();
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
}
實際上,像游戲這種比較復雜的狀態機,包含的狀態比較多,我優先推薦使用查表法,而狀態模式會引入非常多的狀態類,會導致代碼比較難維護,相反,像電商下單、外賣下單這種型別的狀態機,它們的狀態并不多,狀態轉移也比較簡單,但事件觸發執行的動作包含的業務邏輯可能會比較復雜,所以,更加推薦使用狀態模式來實作,
- 第一種實作方式叫分支邏輯法,利用if-else或者switch-case分支邏輯,參照狀態轉移圖,將每一個狀態轉移原模原樣地直譯成代碼,對于簡單的狀態機來說,這種實作方式最簡單、最直接,是首選,
- 第二種實作方式叫查表法,對于狀態很多、狀態轉移比較復雜的狀態機來說,查表法比較合適,通過二維陣列來表示狀態轉移圖,能極大地提高代碼的可讀性和可維護性,
- 第三種實作方式叫狀態模式,對于狀態并不多、狀態轉移也比較簡單,但事件觸發執行的動作包含的業務邏輯可能比較復雜的狀態機來說,我們首選這種實作方式,
在作業的時候,設定了一個超級復雜的狀態轉換機,現在想想其實它是屬于狀態很多、狀態轉移比較復雜,沒有事件觸發會執行業務邏輯,只會進行狀態的變動,每種狀態會被允許做一些對應的操作的情況,應該使用查表法來表示,目前使用的是狀態模式,在做改動的時候還是非常難操作,有很多地方都不太好把握住,類比到下圖中的 / 就應該是:
DrillTaskStatusEnum transferredStatus = state.transfer(drillTaskInstrumentEnum);
if (transferredStatus == DrillTaskStatusEnum.UNDEFINED) {
throw new DrillStatusTransferException("修改任務狀態失敗,當前狀態 = " + currentStatus + ", 不支持操作 = " + drillTaskInstrumentEnum.getDesc() + ", taskId = " + taskId);
}
未定義狀態,報錯即可!!!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/290365.html
標籤:其他
