概述
本章學(xué)習(xí)狀態(tài)模式。在實際的開發(fā)中,狀態(tài)模式并不是很常用,但是在能夠用到的場景里,它可以發(fā)揮很大的作用。從這一點上看,它有點像我們之前講到的組合模式。
狀態(tài)模式一般用來實現(xiàn)狀態(tài)機,而狀態(tài)機常用在游戲、工作流引擎等系統(tǒng)開發(fā)中。不過,狀態(tài)機的實現(xiàn)方式有很多種,除了狀態(tài)模式,比較常用的還有分支邏輯法和查表法。本章就詳細(xì)講講這幾種實現(xiàn)方式,并且對比一下它們的優(yōu)劣和應(yīng)用場景。
什么是有限狀態(tài)機
有限狀態(tài)機,英文翻譯是 Finite State Machine,縮寫為 FSM,簡稱狀態(tài)機。狀態(tài)機有三個組成部分:狀態(tài)(State)、事件(Event)、動作(Action)。
- 事件也稱為轉(zhuǎn)移條件(Transaction Condition)。
- 事件觸發(fā)狀態(tài)的轉(zhuǎn)移及動作的執(zhí)行。
- 不過,動作不是必須得,也可能只轉(zhuǎn)移狀態(tài),不執(zhí)行任何動作。
對于剛剛給出的狀態(tài)機定義,結(jié)合一個具體的例子進(jìn)行解釋。
“超級馬里奧” 游戲不知道你玩過沒有?在游戲中,馬里奧可以變身為多種形態(tài),比如小馬里奧(Small Mario)、超級馬里奧(Super Mario)、火焰馬里奧(Fire Mario)、斗篷馬里奧(Cape Mario)等等。在不同的游戲情節(jié)下,各個形態(tài)會互相轉(zhuǎn)化,并相應(yīng)地增減積分。比如,初始狀態(tài)是小馬里奧,吃了蘑菇后就變成超級馬里奧,并且增加 100 積分。
實際上,馬里奧形態(tài)的轉(zhuǎn)變就是一個狀態(tài)機。其中,馬里奧的不同形態(tài)就是狀態(tài)機種的 “狀態(tài)”,游戲情節(jié)(比如吃了蘑菇)就是狀態(tài)機種的 “事件”,加減積分就是狀態(tài)機種的 “動作”。比如,吃蘑菇這個事件,會觸發(fā)狀態(tài)的轉(zhuǎn)移(從小馬里奧轉(zhuǎn)移到超級馬里奧),以及觸發(fā)動作的執(zhí)行(增加 100 積分)。
為方便講解,我對游戲背景做了簡化,只保留了部分狀態(tài)和事件,簡化之后的狀態(tài)轉(zhuǎn)移如下所示:
如何編程來實現(xiàn)上面的狀態(tài)機呢?
我寫了個骨架代碼,如下所示。其中,obtainMushRoom()
、obtainCape()
、obtainFileFlower()
、meetMonster()
這個幾個函數(shù),能夠根據(jù)當(dāng)前的狀態(tài)和事件,更新狀態(tài)和增減積分。不過,具體的代碼實現(xiàn)暫時沒給出。你可以先試著自己補全一下。
public enum State {
SMALL(0),
SUPER(1),
FIRE(2),
CAPE(3),
;
private int value;
State(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public class MarioStateMachine {
private int score;
private State currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
//TODO
}
public void obtainCape() {
//TODO
}
public void obtainFireFlower() {
//TODO
}
public void meetMonster() {
//TODO
}
public int getScore() {
return score;
}
public State getState() {
return currentState;
}
}
public class ApplicationDemo {
public static void main(String[] args) {
MarioStateMachine mario = new MarioStateMachine();
mario.obtainMushRoom();
int score = mario.getScore();
State state = mario.getState();
System.out.println("mario score: " + score + "; state: " + state);
}
}
狀態(tài)機實現(xiàn)方式一:分支邏輯法
對于如何實現(xiàn)狀態(tài)機,有三種方式。其中,最簡單直接的實現(xiàn)方式是,參照狀態(tài)轉(zhuǎn)移圖,將每一個狀態(tài)轉(zhuǎn)移,原模原樣地直譯成代碼。這樣編寫的代碼會包含大量的 if-esle 或 switch-else 分支判斷邏輯,甚至是嵌套的分支判斷邏輯,所以,我把這種方法暫且命名為分支邏輯法。
按照這個思路,將上面的骨架代碼補全一下。補全之后的代碼如下所示:
public class MarioStateMachine {
private int score;
private State currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
if (currentState == State.SMALL) {
this.currentState = State.SUPER;
this.score += 100;
}
}
public void obtainCape() {
if (currentState == State.SMALL || currentState == State.SUPER) {
this.currentState = State.CAPE;
this.score += 200;
}
}
public void obtainFireFlower() {
if (currentState == State.SMALL || currentState == State.SUPER) {
this.currentState = State.FIRE;
this.score += 300;
}
}
public void meetMonster() {
if (currentState == State.SUPER) {
this.currentState = State.SMALL;
this.score -= 100;
}
if (currentState == State.CAPE) {
this.currentState = State.SMALL;
this.score -= 200;
}
if (currentState == State.FIRE) {
this.currentState = State.SMALL;
this.score -= 300;
}
}
public int getScore() {
return score;
}
public State getState() {
return currentState;
}
}
對于簡單的狀態(tài)機來說,分支邏輯這種實現(xiàn)方式是可以接收的。但是,對于復(fù)雜的狀態(tài)機來說,這種實現(xiàn)方式及其容易漏寫或錯寫某個狀態(tài)轉(zhuǎn)移。此外,代碼中充斥著大量的 if-else 或者 switch-case 分支判斷邏輯,可讀性和可維護(hù)性都很差。如果哪些修改了狀態(tài)機種的某個轉(zhuǎn)移,我們要在冗長的分支邏輯中找到對應(yīng)地代碼進(jìn)行修改,很容易改錯,引入 BUG。
狀態(tài)機實現(xiàn)方式二:查表法
實際上,上面的實現(xiàn)方式有點類似 hard code,對于復(fù)雜的狀態(tài)機來說不適用,而狀態(tài)機的第二種實現(xiàn)方式查表法,就更加合適了。接下來,看下如何利用查表法來補全骨架代碼。
實際上,除了用狀態(tài)轉(zhuǎn)移圖來表示之外,狀態(tài)機還可以用二維表來表示,如下所示。這個二維表中,第一維表示當(dāng)前的狀態(tài),第二維表示當(dāng)前狀態(tài)經(jīng)過事件之后,轉(zhuǎn)移到的新狀態(tài)及其執(zhí)行的動作。
E1(Got MushRoom) | E2(Got Cape) | E3(Got Fire Flower) | E4(Meet Monster) | |
---|---|---|---|---|
Small | Super/+100 | Cape/+200 | Fire/+300 | / |
Super | / | Cape/+200 | Fire/+300 | Small/-100 |
Cape | / | / | / | Small/-200 |
Fire | / | / | / | Small/-300 |
注:表中的斜杠表示不存在這種狀態(tài)轉(zhuǎn)移
相對于分支邏輯的實現(xiàn)方式,查表法的代碼實現(xiàn)更加清晰,可讀性和可維護(hù)性更好。當(dāng)修改狀態(tài)機時,我們只需要修改 transactionTable
和 actionTable
兩個二維數(shù)組即可。實際上,如果我們把二維數(shù)組存儲在配置文件中,當(dāng)需要修改狀態(tài)機時,甚至可以不修改代碼,只需要修改配置文件就可以了。具體代碼如下所示:
public enum Event {
GOT_MUSHROOM(0),
GOT_CAPE(1),
GOT_FIRE(2),
MEET_MONSTER(3),
;
private int value;
Event(int value) {
this.value = value;
}
public int getValue() {
return 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.MEET_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 score;
}
public State getState() {
return currentState;
}
}
狀態(tài)機實現(xiàn)方式三:狀態(tài)模式
在查表法的代碼實現(xiàn)中,事件觸發(fā)的動作只能是簡單的積分加減,所以,我們用一個 int 類型的二維數(shù)組 actionTable
就能表示,二維數(shù)組中的值表示積分的加減值。但是,如果要執(zhí)行的動作并非這么簡單,而是一系列復(fù)雜的邏輯操作(比如加減積分、寫數(shù)據(jù)庫,還有可能發(fā)送消息通知等等),我們就沒法用如此簡單的二維數(shù)組來表示了。也就是說,查表法的實現(xiàn)方式有一定的局限性。
雖然分支邏輯的實現(xiàn)方式不存在這個問題,但它又存在前面講到的其他問題,比如分支判斷邏輯較多,導(dǎo)致代碼的可讀性和可維護(hù)性不好等。實際上,對于分支邏輯法存在的問題,可以使用狀態(tài)模式來解決。
狀態(tài)模式通過將事件觸發(fā)的狀態(tài)轉(zhuǎn)移和動作執(zhí)行,拆分到不同的類中,來避免分支判斷邏輯。我們還是結(jié)合代碼來理解這句話。
其中,IMario
是狀態(tài)的接口,定義了所有事件。SmallMario
、SuperMario
、CapeMario
、FireMario
是 IMario
接口的實現(xiàn)類,分別對應(yīng)狀態(tài)機種的 4 個狀態(tài)。原來所有的狀態(tài)轉(zhuǎn)移和動作執(zhí)行的代碼邏輯,都集中在 MarioStateMachine
中,現(xiàn)在,這些代碼被拆分到了這 4 個狀態(tài)類中。
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);
}
}
public class CapeMario implements IMario {
private MarioStateMachine stateMachine;
public CapeMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.CAPE;
}
@Override
public void obtainMushRoom() {
// do nothing...
}
@Override
public void obtainCape() {
// do nothing...
}
@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() - 200);
}
}
public class FireMario implements IMario {
private MarioStateMachine stateMachine;
public FireMario(MarioStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.FIRE;
}
@Override
public void obtainMushRoom() {
// do nothing...
}
@Override
public void obtainCape() {
// do nothing...
}
@Override
public void obtainFireFlower() {
// do nothing...
}
@Override
public void meetMonster() {
stateMachine.setCurrentState(new SmallMario(stateMachine));
stateMachine.setScore(stateMachine.getScore() - 300);
}
}
public class MarioStateMachine {
private int score;
private IMario currentState; // 不在使用枚舉表示狀態(tài)
public MarioStateMachine() {
this.score = 0;
this.currentState = new SmallMario(this);
}
public void obtainMushRoom() {
currentState.obtainMushRoom();
}
public void obtainCape() {
currentState.obtainCape();
}
public void obtainFireFlower() {
currentState.obtainFireFlower();
}
public void meetMonster() {
currentState.meetMonster();
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
public IMario getState() {
return currentState;
}
}
上面的代碼實現(xiàn)不難看懂,只需要注意一點,即 MarioStateMachine
和各個狀態(tài)類之間是雙向依賴關(guān)系。 MarioStateMachine
依賴各個類是理所當(dāng)然的,但是反過來,各個狀態(tài)類為什么要依賴 MarioStateMachine
呢? 這是因為,各個狀態(tài)類需要更新 MarioStateMachine
中的屬性, score
和 currentState
。
實際上,上面的代碼還可以繼續(xù)優(yōu)化,可以將狀態(tài)類設(shè)置成單例,比較狀態(tài)類中不包含任何成員變量。但是,當(dāng)狀態(tài)類設(shè)計成單例之后,就無法通過構(gòu)造函數(shù)來傳遞 MarioStateMachine
了,而狀態(tài)類又要依賴 MarioStateMachine
,那該如何解決呢?
實際上,在《創(chuàng)建型:2.單例模式(中):為什么不推薦使用單例模式?又有何替代方案?》中,提到過集中解決方法,你可以回過頭去查看下。在這里,可以通過函數(shù)參數(shù)將 MarioStateMachine
傳遞進(jìn)狀態(tài)類。根據(jù)這個設(shè)計思路,對上面的代碼進(jìn)行重構(gòu)。重構(gòu)之后的代碼如下所示:
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...
}
}
public class SuperMario implements IMario {
private static final SuperMario instance = new SuperMario();
private SuperMario() {
}
public static SuperMario getInstance() {
return instance;
}
@Override
public State getName() {
return State.SUPER;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
// do nothing...
}
@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) {
stateMachine.setCurrentState(SmallMario.getInstance());
stateMachine.setScore(stateMachine.getScore() - 100);
}
}
public class CapeMario implements IMario {
private static final CapeMario instance = new CapeMario();
private CapeMario() {
}
public static CapeMario getInstance() {
return instance;
}
@Override
public State getName() {
return State.CAPE;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
// do nothing...
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
// do nothing...
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(FireMario.getInstance());
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(SmallMario.getInstance());
stateMachine.setScore(stateMachine.getScore() - 200);
}
}
public class FireMario implements IMario {
private static final FireMario instance = new FireMario();
private FireMario() {
}
public static FireMario getInstance() {
return instance;
}
@Override
public State getName() {
return State.FIRE;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
// do nothing...
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
// do nothing...
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
// do nothing...
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
stateMachine.setCurrentState(SmallMario.getInstance());
stateMachine.setScore(stateMachine.getScore() - 300);
}
}
public class MarioStateMachine {
private int score;
private IMario currentState; // 不在使用枚舉表示狀態(tài)
public MarioStateMachine() {
this.score = 0;
this.currentState = SmallMario.getInstance();
}
public void obtainMushRoom() {
currentState.obtainMushRoom(this);
}
public void obtainCape() {
currentState.obtainCape(this);
}
public void obtainFireFlower() {
currentState.obtainFireFlower(this);
}
public void meetMonster() {
currentState.meetMonster(this);
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
public IMario getState() {
return currentState;
}
}
實際上,像游戲這種比較復(fù)雜的狀態(tài)機,包含的狀態(tài)比較多,優(yōu)先推薦使用查表法,而狀態(tài)模式會引入非常多的狀態(tài)類,會導(dǎo)致代碼比較難維護(hù)。
相反,像電商下單、外賣下單這種類型的狀態(tài)機,它們的狀態(tài)并不多,狀態(tài)轉(zhuǎn)移也比較簡單,但事件觸發(fā)執(zhí)行的動作包含的業(yè)務(wù)邏輯可能會比較復(fù)雜,所以更加推薦使用狀態(tài)模式來實現(xiàn)。
總結(jié)
本章講解了狀態(tài)模式。雖然網(wǎng)上有各種各樣的狀態(tài)模式,但是你只要記住狀態(tài)模式是狀態(tài)機的一種實現(xiàn)方式即可。
狀態(tài)機又叫有限狀態(tài)機,它由3部分組成:狀態(tài)、事件、動作。文章來源:http://www.zghlxwxcb.cn/news/detail-846343.html
- 其中事件也稱為轉(zhuǎn)移條件。
- 事件觸發(fā)狀態(tài)的轉(zhuǎn)移及動作的執(zhí)行。
- 不過動作不是必須的,也可能只轉(zhuǎn)移狀態(tài),不執(zhí)行任何動作。
針對狀態(tài)機,本章總結(jié)了三種實現(xiàn)方式。文章來源地址http://www.zghlxwxcb.cn/news/detail-846343.html
- 第一種實現(xiàn)方式叫分支邏輯法。利用 if-else 或 switch-case 分支邏輯,參照狀態(tài)轉(zhuǎn)移圖,將每個狀態(tài)轉(zhuǎn)移原模原樣的直譯成代碼。對于簡單的狀態(tài)機來說,這種實現(xiàn)方式最簡單、最直接,是首選。
- 第二種實現(xiàn)方式叫查表法。對于狀態(tài)很多、狀態(tài)轉(zhuǎn)移比較復(fù)雜的狀態(tài)機來說,查表法比較合適。通過二維素組來表示狀態(tài)轉(zhuǎn)移圖,能極大地提高代碼的可讀性和可維護(hù)性。
- 第三張實現(xiàn)方式叫狀態(tài)模式。對于狀態(tài)不多、狀態(tài)轉(zhuǎn)移也比較簡單,但事件觸發(fā)執(zhí)行的動作包含的業(yè)務(wù)邏輯可能比較復(fù)雜的狀態(tài)機來說,首選這種實現(xiàn)方式。
到了這里,關(guān)于設(shè)計模式學(xué)習(xí)筆記 - 設(shè)計模式與范式 -行為型:8.狀態(tài)模式:游戲、工作流引擎中常用的狀態(tài)機是如何實現(xiàn)的?的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!