状态机模式

如果大家觉得文章有错误内容,欢迎留言或者私信讨论~

  状态模式在实际的软件开发中并不是很常用,一般都是用来实现状态机的,而状态机都是应用在游戏、工作流引擎等系统的开发中。
  话不多说,开始吧。

状态机是什么?

  根据定义,状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

  举一个例子,我们耳熟能详的游戏——超级马里奥,它就能根据吃到的道具变身多种形态:超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同游戏场景下,各个形态会互相转化,还会获得积分。
  实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状
态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。
  你可以看图理解:

  • E1:吃了蘑菇
  • E2:获得斗篷
  • E3:获得火焰
  • E4:遇到怪物

在这里插入图片描述
  我们如何编程获得上面的状态机呢?换句话,如何将上面的代码翻译成代码呢?

  这里提供一个骨架代码,你可以将它作为面试题补全。如下所示。其中,obtainMushRoom()、obtainCape()、obtainFireFlower()、meetMonster() 这几个函数,能够根据当前的状态和事件,更新状态和增减积分。

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;
	}
}

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 this.score;
	}

	public State getCurrentState() {
		return this.currentState;
	}
}

public class ApplicationDemo {
	public static void main(String[] args) {
		MarioStateMachine mario = new MarioStateMachine();
		mario.obtainMushRoom();
		int score = mario.getScore();
		State state = mario.getCurrentState();
		System.out.println("mario score: " + score + "; state: " + state);
	}
}

实现方式一:分支逻辑法

  这也是最简单的一种,参照状态转移图,将一种状态每一种情况都原模原样地直译为代码,当然这样会包含大量的if-else,如下:

public class MarioStateMachine {
	private int score;
	private State currentState;

	public MarioStateMachine() {
		this.score = 0;
		this.currentState = State.SMALL;
	}

	public void obtainMushRoom() {
		if (currentState.equals(State.SMALL)) {
			this.currentState = State.SUPER;
			this.score += 100;
		}
	}

	
	public void obtainCape() {
		if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) )
			this.currentState = State.CAPE;
			this.score += 200;
		}
	} 
	
	public void obtainFireFlower() {
		if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) )
			this.currentState = State.FIRE;
			this.score += 300;
		}
	}

	public void meetMonster() {
		if (currentState.equals(State.SUPER)) {
			this.currentState = State.SMALL;
			this.score -= 100;
			return;
		}
		
		if (currentState.equals(State.CAPE)) {
			this.currentState = State.SMALL;
			this.score -= 200;
			return;
		}
		
		if (currentState.equals(State.FIRE)) {
			this.currentState = State.SMALL;
			this.score -= 300;
			return;
		}
	}

	public int getScore() {
		return this.score;
	} 
	
	public State getCurrentState() {
		return this.currentState;
	}
}

  当然这种仅仅只适合简单的状态,对于复杂的状态机来说,很容易漏写或者错写某个状态转移,并且大量的 if-else 也会让代码的维护性和可读性变差,导致后续接手的同学骂娘。

实现方式二:查表法

  实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。
在这里插入图片描述
  相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。具体的代码如下所示:

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;
	}
}

实现方法之三:状态模式

  在查表法的代码实现中,事件触发的动作只是简单的积分加减,所以我们用一个二维数组就能轻易解决。但是如果要执行的动作并不简单,比如加减积分、写数据库,还有可能发送消息通知等等。
  所以又有了状态模式,状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。我们结合代码来理解。
  利用状态模式,我们来补全 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;
	}
}

  实际上,像游戏这种比较复杂的状态机,包含的状态比较多,我优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值