状态模式在实际的软件开发中并不常用,但在能够用到它的场景能够发挥重要作用。状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统的开发中。不过状态台机的实现有多种,比较常用的是分支逻辑法和查表法
状态模式,英文翻译State Pattern。允许对象内部状态发生改变时,对象看起来好像修改了它的类。
主要解决:对象行为依赖于它的状态(属性),并且可以根据它的状态来改变它的相关行为。一般用来实现状态机
什么是有限状态机?
有限状态机,英文翻译Finite State Machine,英文缩写FSM,简称为状态机。状态机有三个组成部分:状态(state)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移以及动作的执行。不过,动作不是必须的,也可能只是转移状态,不执行任何动作。
上面给出了状态机的定义,那结合“超级马里奥”这款游戏来进行说明:
在游戏中,马里奥可有变身为多种形态,比如:小马里奥(Small Mario)、超级马里奥(Super Mario)、斗篷马里奥(Cape Mario)、火焰马里奥(fire Mario)等等,在不同的游戏情节下,各种形态相互转换,并且相应的增减积分。比如:初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加100积分。
实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的不同"状态",游戏情节(比如吃蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个"事件",会触发状态转移:从小马里奥变身为超级马里奥,以及触发“动作”的执行(增加100积分)。
下面对游戏进行了简化,只保留了部分事件和状态,简化后的状态转移如下图所示:
如何实现上面的状态机呢?首先会先给出骨架代码,然后给出使用if-else分支逻辑法的实现方式,最后会使用状态模式的方式进行实现。
马里奥状态机的骨架代码
如下所示:状态机中包含,吃了蘑菇(obtainMushroom())、获得斗篷(obtainCape)、获得火焰(obtainFireFlower())、遇到怪物(meetMonster())这几个事件对应的方法。这些方法会根据当前状态和事件,更新状态和增减积分。
骨架代码代码如下:
/**
* 马里奥的状态枚举类
*/
public enum MarioState {
SMALL(0,"小马里奥"),
SUPPER(1,"超级马里奥"),
CAPE(2,"斗篷马里奥"),
FIRE(3,"火焰马里奥"),
;
private int state;
private String remark;
MarioState(int state, String remark) {
this.state = state;
this.remark = remark;
}
public int getState() {
return state;
}
public String getRemark() {
return remark;
}
}
/**
* 马里奥状态机
*/
public class MairoStateMachine {
private int score;
private MarioState currentState;
public MairoStateMachine() {
this.score=0;
currentState=MarioState.SMALL;
}
//吃了蘑菇
public void obtainMushroom(){
}
//获得斗篷
public void obtainCape(){
}
//获得火焰
public void obtainFireFlower(){
}
//碰到小怪物
public void meetMonster(){
}
public int getScore() {
return score;
}
public MarioState getCurrentState() {
return currentState;
}
}
/**
* 测试
*/
public class Demo {
@Test
public void test() {
MairoStateMachine mario=new MairoStateMachine();
mario.obtainMushroom();
int score=mario.getScore();
MarioState state=mario.getCurrentState();
System.out.println("mario score:"+ score +"; state:"+state.getRemark());
}
}
if-else分支逻辑法实现
上面状态图中,“动作”这一部分比较简单。直接参照上面的状态图,将每一个状态转移和动作的执行直接翻译为代码就可以了。这样编写的代码会存在缺陷,存在大量的if-else。
分支逻辑法实现代码如下:
/**
* 马里奥状态机
*/
public class MairoStateMachine {
private int score;
private MarioState currentState;
public MairoStateMachine() {
this.score=0;
currentState=MarioState.SMALL;
}
//吃了蘑菇
public void obtainMushroom(){
if(currentState.equals(MarioState.SMALL)){
this.currentState = MarioState.SUPPER; //状态更新
score+=100; //执行"动作"
}
}
//获得斗篷
public void obtainCape(){
if(currentState.equals(MarioState.SMALL) || currentState.equals(MarioState.SUPPER)){
this.currentState = MarioState.CAPE;
score+=200;
}
}
//获得火焰
public void obtainFireFlower(){
if(currentState.equals(MarioState.SMALL) || currentState.equals(MarioState.SUPPER)){
this.currentState = MarioState.FIRE;
score+=300;
}
}
//碰到小怪物
public void meetMonster(){
if(currentState.equals(MarioState.SUPPER)){
this.currentState = MarioState.SMALL;
score-=100;
}else if(currentState.equals(MarioState.CAPE)){
this.currentState = MarioState.SMALL;
score-=200;
}else if(currentState.equals(MarioState.FIRE)){
this.currentState = MarioState.SMALL;
score-=300;
}
}
public int getScore() {
return score;
}
public MarioState getCurrentState() {
return currentState;
}
}
如果状态机中的“动作”简单,事件也不多的话,这样实现是完全没问题的。
状态模式实现
对于设计模式学习,画出类图,可以加深对模式的理解,类图如下:
其中IMario是状态的接口,定义了所有事件。SmallMario、SupperMario、CapeMario、FireMario是IMario接口的实现类。分别对应着状态机中的4个状态。代码实现如下。
/**
* 马里奥的状态接口类
*/
public interface IMario {
MarioState getName();
void obtainMushroom();
void obtainCape();
void obtainFireFlower();
void meetMonster();
}
/**
* 马里奥的状态抽线类
*
* 用于具体的状态类只重写自己所拥有的事件,屏蔽其没有拥有的事件
*/
public abstract class AbstraceMario implements IMario {
protected MarioStateMachine marioStateMachine;
public AbstraceMario(MarioStateMachine marioStateMachine) {
this.marioStateMachine = marioStateMachine;
}
@Override
public MarioState getName() {return null;}
@Override
public void obtainMushroom() {}
@Override
public void obtainCape() {}
@Override
public void obtainFireFlower() {}
@Override
public void meetMonster() {}
}
/**
* 小马里奥
*
* 用于处理小马里奥所用于的全部事件
*/
public class SmallMario extends AbstraceMario {
public SmallMario(MarioStateMachine marioStateMachine) {
super(marioStateMachine);
}
@Override
public MarioState getName() {
return MarioState.SMALL;
}
@Override
public void obtainMushroom() {
marioStateMachine.setCurrentState(new SupperMario(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()+100);
}
@Override
public void obtainCape() {
marioStateMachine.setCurrentState(new CapeMario(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()+200);
}
@Override
public void obtainFireFlower() {
marioStateMachine.setCurrentState(new FireMairo(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()+3300);
}
}
/**
* 超级马里奥
*/
public class SupperMario extends AbstraceMario {
public SupperMario(MarioStateMachine marioStateMachine) {
super(marioStateMachine);
}
@Override
public MarioState getName() {
return MarioState.SUPPER;
}
@Override
public void obtainCape() {
marioStateMachine.setCurrentState(new CapeMario(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()+200);
}
@Override
public void obtainFireFlower() {
marioStateMachine.setCurrentState(new FireMairo(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()+300);
}
@Override
public void meetMonster() {
marioStateMachine.setCurrentState(new SupperMario(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()-100);
}
}
/**
* 斗篷马里奥
*/
public class CapeMario extends AbstraceMario {
public CapeMario(MarioStateMachine marioStateMachine) {
super(marioStateMachine);
}
@Override
public MarioState getName() {
return MarioState.CAPE;
}
@Override
public void meetMonster() {
marioStateMachine.setCurrentState(new SupperMario(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()-100);
}
}
/**
* 火焰马里奥
*/
public class FireMairo extends AbstraceMario {
public FireMairo(MarioStateMachine marioStateMachine) {
super(marioStateMachine);
}
@Override
public MarioState getName() {
return MarioState.FIRE;
}
@Override
public void meetMonster() {
marioStateMachine.setCurrentState(new SupperMario(marioStateMachine));
marioStateMachine.setScore(marioStateMachine.getScore()-100);
}
}
/**
* 马里奥状态机
*/
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 score;
}
public IMario getCurrentState() {
return currentState;
}
public void setScore(int score) {
this.score = score;
}
public void setCurrentState(IMario currentState) {
this.currentState = currentState;
}
}
测试代码:
@Test
public void test(){
MarioStateMachine stateMachine = new MarioStateMachine();
stateMachine.obtainMushroom();
stateMachine.obtainFireFlower();
int score=stateMachine.getScore();
IMario state=stateMachine.getCurrentState();
System.out.println("mario score:"+ score +"; state:"+state.getName().getRemark());
}
执行结果:
从上面代码可以看出,原来所有的状态转移和动作代码执行逻辑,都集中在MarioStateMachine 。现在这些代码逻辑被分散到了各个状态类中。。
这里有一点需要关注,MarioStateMachine和各个状态类之间是双向依赖关系。MarioStateMachine依赖状态类是理所当然的,而反过来,状态类为什么要依赖MarioStateMachine呢?因为各个状态类需要更新MarioStateMachine中的score 和 currentState。
总结:
状态模式:需要定义一个状态接口,包含了状态机中的所有事件,有多少个状态,就有多少个状态类。状态类中的事件需要完成两件事,一件是业务逻辑的实现,一件是状态的转移。
如果业务逻辑简单,比较推荐使用if-else分支。
如果业务逻辑比较复杂,那就推荐使用状态机来实现。
参考:设计模式之美--王争