设计模式-状态模式
状态模式其实某些业务系统中经常用到,比如某些报名审核系统,报名状态之间有比较复杂的转换,例如刚开始注册是待提交,提交后是待一级审核,此时可以进行退回、也可以选择审核通过/不通过,审核通过后还能进行上级审核等。
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。
状态机中有三个核心元素:状态(State)、事件(Event)和动作(Action)。其中,事件也被称为转移条件,触发状态的转换和动作的执行,不过动作执行并不是必须的。
案例分析
有一个马里奥的游戏,初始时是最小的,期间可以吃蘑菇变成超级马里奥,获得斗篷变成斗篷马里奥等,不同的操作会对得分进行变更。

假设这个游戏只能有以上这些转换,不会死亡直到通过,有以下几种实现方式
分支逻辑法
这种方法是我们最常用的,定义一个枚举类包含可能出现的状态
java
public enum State {
SMALL(0),
SUPER(1),
FIRE(2),
CAPE(3);
private int value;
private State(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
定义一个状态机上下文,属性包括状态和得分,为每种事件定义一个处理方法:
java
public class MarioStateMachine {
private int score;
private State currentState;
public MarioStateMachine() {
this.score = 0;
this.currentState = State.SMALL;
}
public void obtainMushRoom() {
if (State.SMALL.getValue() == currentState.getValue()) {
score += 100;
currentState = State.SUPER;
}
}
public void obtainCape() {
if (State.SMALL.getValue() == currentState.getValue()) {
score += 200;
currentState = State.CAPE;
return;
}
if (State.SUPER.getValue() == currentState.getValue()) {
score += 200;
currentState = State.CAPE;
}
}
public void obtainFireFlower() {
if (State.SMALL.getValue() == currentState.getValue()) {
score += 300;
currentState = State.FIRE;
return;
}
if (State.SUPER.getValue() == currentState.getValue()) {
score += 300;
currentState = State.FIRE;
}
}
public void meetMonster() {
if (State.SUPER.getValue() == currentState.getValue()) {
score -= 100;
currentState = State.SMALL;
return;
}
if (State.CAPE.getValue() == currentState.getValue()) {
score -= 200;
currentState = State.SMALL;
return;
}
if (State.FIRE.getValue() == currentState.getValue()) {
score -= 300;
currentState = State.SMALL;
}
}
public int getScore() {
return this.score;
}
public State getCurrentState() {
return this.currentState;
}
}
这种在转换简单的时候易于实现,但是在逻辑复杂时可能导致类代码过于庞大,新增或删除状态时需要查找有影响的地方并进行修改,在状态多且频繁变更的情况下难以维护。
查表法
案例事件触发的动作是非常简单的,只需要对分数进行对应变化即可,针对这类情况,我们可以定义两个二维数组,分别表示【原始状态】经过某个【事件】后最终的状态和需要变化的分数。

例如在 Small 状态下吃到蘑菇(Got MushRoom)就会变为 Super,得分 +100。
java
// 定义事件
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;
}
}
java
public class MarioStateMachine {
private int score;
private State currentState;
private static final State[][] transitionTable = {
{State.SUPER, State.CAPE, State.FIRE, State.SMALL},
{State.SUPER, State.CAPE, State.FIRE, State.SMALL},
{State.CAPE, State.CAPE, State.CAPE, State.SMALL},
{State.FIRE, State.FIRE, State.FIRE, State.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;
}
}
状态模式
查表法是因为触发的动作非常简单,仅仅是对得分进行了修改,如果触发的动作非常复杂,例如需要更新表状态,生成待办等不方便用数组存储且逻辑依然耦合在大的处理类中。
利用状态模式我们定义一个接口,接口中包含可能遇到的所有事件
java
public interface MarioStateService {
void obtainMushRoom(MarioStateMachine stateMachine);
void obtainCape(MarioStateMachine stateMachine);
void obtainFireFlower(MarioStateMachine stateMachine);
void meetMonster(MarioStateMachine stateMachine);
State getState();
}
四种形态的马里奥都实现这个接口
java
public class SmallMarioStateService implements MarioStateService {
private static final SmallMarioStateService instance = new SmallMarioStateService();
public static MarioStateService getInstance() {
return instance;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(SuperMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() + 100);
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(CapeMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(FireMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
}
@Override
public State getState() {
return State.SMALL;
}
}
java
public class SuperMarioStateService implements MarioStateService {
private static final SuperMarioStateService instance = new SuperMarioStateService();
public static MarioStateService getInstance() {
return instance;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(CapeMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() + 200);
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(FireMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() + 300);
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(SmallMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() - 100);
}
@Override
public State getState() {
return State.SUPER;
}
}
java
public class CapeMarioStateService implements MarioStateService {
private static final CapeMarioStateService instance = new CapeMarioStateService();
public static MarioStateService getInstance() {
return instance;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(SmallMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() - 200);
}
@Override
public State getState() {
return State.CAPE;
}
}
java
public class FireMarioStateService implements MarioStateService {
private static final FireMarioStateService instance = new FireMarioStateService();
public static MarioStateService getInstance() {
return instance;
}
@Override
public void obtainMushRoom(MarioStateMachine stateMachine) {
}
@Override
public void obtainCape(MarioStateMachine stateMachine) {
}
@Override
public void obtainFireFlower(MarioStateMachine stateMachine) {
}
@Override
public void meetMonster(MarioStateMachine stateMachine) {
stateMachine.setCurrentStateService(SmallMarioStateService.getInstance());
stateMachine.setScore(stateMachine.getScore() - 300);
}
@Override
public State getState() {
return State.FIRE;
}
}
定义状态机类
java
public class MarioStateMachine {
@Getter
@Setter
private int score;
@Getter
@Setter
private MarioStateService currentStateService;
public MarioStateMachine() {
this.score = 0;
this.currentStateService = SmallMarioStateService.getInstance();
}
public void obtainMushRoom() {
currentStateService.obtainMushRoom(this);
}
public void obtainCape() {
currentStateService.obtainCape(this);
}
public void obtainFireFlower() {
currentStateService.obtainFireFlower(this);
}
public void meetMonster() {
currentStateService.meetMonster(this);
}
public State getState() {
return currentStateService.getState();
}
}
利用多态特性,使用接口 MarioStateService 存储当前真正的状态对象,并在遇到事件时调用当前真正对象的处理方法,并将状态对象设计为无状态的单例,多个状态机实例可以复用同一个状态对象。
不过该实现不够优雅,原因是按照目前的规则,例如 Fire(火焰形态)只会遇到一种事件就是遇到怪物,但是它依然需要实现所有的方法,该怎么设计解决这个问题呢?
第一种方案可以将给接口设计为 default 默认空实现,由实现类主动选择可能遇到的事件并进行重写。
第二种方案可以给接口一个抽象实现类,getState() 方法设计为抽象方法要求子类必须实现,其余接口全部空实现,由继承者主要选择并重写,其实和第一种方案思路一致。
第三种方案可以根据情况设计相应的实现类,例如 Fire 形态和 Cape 形态都只会处理遇到怪物事件,定义一个类实现接口并给其他接口空实现,FireImpl 和 CapeImpl 类继承这个实现类。