状态模式的主要组成部分如下:
- 上下文(Context):上下文通常包含一个具体状态的引用,用于维护当前状态。上下文委托给当前状态对象处理状态相关行为。
- 抽象状态(State):定义一个接口,用于封装与上下文的特定状态相关的行为。
- 具体状态(Concrete State):实现抽象状态接口,为具体状态定义行为。每个具体状态类对应一个状态。
假设要模拟一个简易的电视遥控器,具有开启、关闭和调整音量的功能。
如果不使用设计模式,编写出来的代码可能是这个样子的,需要针对电视机当前的状态为每一次操作编写判断逻辑:
public class TV {
private boolean isOn;
private int volume;
public TV() {
isOn = false;
volume = 0;
}
public void turnOn() {
// 如果是开启状态
if (isOn) {
System.out.println("TV is already on.");
// 否则打开电视
} else {
isOn = true;
System.out.println("Turning on the TV.");
}
}
public void turnOff() {
if (isOn) {
isOn = false;
System.out.println("Turning off the TV.");
} else {
System.out.println("TV is already off.");
}
}
public void adjustVolume(int volume) {
if (isOn) {
this.volume = volume;
System.out.println("Adjusting volume to: " + volume);
} else {
System.out.println("Cannot adjust volume, TV is off.");
}
}
}
public class Main {
public static void main(String[] args) {
TV tv = new TV();
tv.turnOn();
tv.adjustVolume(10);
tv.turnOff();
}
}
在该例子中状态比较少,所以代码看起来也不是很复杂,但是状态如果变多了呢?比如加入换台,快捷键、静音等功能后呢?会发现条件分支会急速膨胀,所以此时状态设计模式就要登场了:
定义抽象状态接口 TVState
,将每一个修改状态的动作抽象成一个接口:
public interface TVState {
void turnOn();
void turnOff();
void adjustVolume(int volume);
}
为每个具体状态创建类,实现 TVState
接口。例如,创建 TVOnState
和 TVOffState
类:
// 在on状态下,去执行以下各种操作
public class TVOnState implements TVState {
@Override
public void turnOn() {
System.out.println("TV is already on.");
}
@Override
public void turnOff() {
System.out.println("Turning off the TV.");
}
@Override
public void adjustVolume(int volume) {
System.out.println("Adjusting volume to: " + volume);
}
}
// 在关机的状态下执行以下的操作
public class TVOffState implements TVState {
@Override
public void turnOn() {
System.out.println("Turning on the TV.");
}
@Override
public void turnOff() {
System.out.println("TV is already off.");
}
@Override
public void adjustVolume(int volume) {
System.out.println("Cannot adjust volume, TV is off.");
}
}
定义上下文类 TV
:
public class TV {
// 当前状态
private TVState state;
public TV() {
state = new TVOffState();
}
public void setState(TVState state) {
this.state = state;
}
public void turnOn() {
// 打开
state.turnOn();
// 设置为开机状态
setState(new TVOnState());
}
public void turnOff() {
// 关闭
state.turnOff();
// 设置为关机状态
setState(new TVOffState());
}
public void adjustVolume(int volume) {
state.adjustVolume(volume);
}
}
通过以下方式使用这些类:
public class Main {
public static void main(String[] args) {
TV tv = new TV();
tv.turnOn();
tv.adjustVolume(10);
tv.turnOff();
}
}
有限状态机
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。
有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机,比较官方的说法是:有限状态机是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。。状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
“超级马里奥”游戏中,马里奥可以变身为三种形态,小马里奥(Small Mario)、大马里奥(Big Mario)和火焰马里奥(Fire Mario)。马里奥可以通过吃蘑菇(Mushroom)、火花(Fire Flower)或被敌人攻击(Enemy Attack)来改变形态。将用状态图表示这个马里奥的有限状态机。
这个状态图描述了马里奥角色在游戏中的状态转换。可以根据这个有限状态机来实现一个马里奥游戏的简化版本。在实际游戏开发中,通常会使用游戏引擎或编程框架来处理状态转换,而不是手动编写状态机代码。
分支法
对于如何实现状态机,总结了三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,把这种方法暂且命名为分支法。
下面是一个使用if-else 语句实现的马里奥形态变化的代码示例:
public class Mario {
private MarioState state;
public Mario() {
state = MarioState.SMALL;
}
public void handleEvent(Event event) {
MarioState newState = state;
// 处理吃蘑菇事件
if (event == Event.MUSHROOM) {
if (state == MarioState.SMALL) {
newState = MarioState.BIG;
}
// 处理吃火花事件
} else if (event == Event.FIRE_FLOWER) {
if (state == MarioState.BIG) {
newState = MarioState.FIRE;
}
// 处理遇到小怪事件
} else if (event == Event.ENEMY_ATTACK) {
if (state == MarioState.BIG) {
newState = MarioState.SMALL;
} else if (state == MarioState.FIRE) {
newState = MarioState.BIG;
} else if (state == MarioState.SMALL) {
newState = MarioState.DEAD;
}
// 处理掉坑事件
} else if (event == Event.FALL_INTO_PIT) {
newState = MarioState.DEAD;
}
System.out.printf("从 %s 变为 %s%n", state, newState);
state = newState;
}
}
public class MarioDemo {
public static void main(String[] args) {
Mario mario = new Mario();
mario.handleEvent(Event.MUSHROOM); // 变为大马里奥
mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥
mario.handleEvent(Event.ENEMY_ATTACK); // 变为死亡马里奥
}
}
使用 if-else 语句来处理状态转换。在 handleEvent
方法中,根据事件和当前状态的组合来确定新状态,并更新马里奥的状态。这种实现方法相较于查表法和面向对象实现更为简单,但可能在状态和事件更多的情况下变得难以维护。选择合适的实现方法取决于实际需求和场景。
查表法
这种实现方法有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。如何利用查表法来补全骨架代码。
可以将马里奥的状态转移方式表示为以下表格:
当前状态/事件 | MUSHROOM | FIRE_FLOWER | ENEMY_ATTACK | FALL_INTO_PIT |
SMALL | BIG | SMALL | DEAD | DEAD |
BIG | BIG | FIRE | SMALL | DEAD |
FIRE | FIRE | FIRE | BIG | DEAD |
DEAD | DEAD | DEAD | DEAD | DEAD |
这个表格显示了马里奥在不同状态下遇到不同事件时将转换为的新状态。从左到右分别表示当前状态(SMALL, BIG, FIRE, DEAD),从上到下表示事件(MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT)。表格中的每个单元格表示对应状态和事件的状态转换结果。
查表法是一种使用查找表来处理状态转换的方法,可以简化状态机的实现。以下是使用查表法实现马里奥形态变化的代码示例:
enum MarioState {
SMALL, BIG, FIRE, DEAD
}
enum Event {
MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT
}
public class Mario {
private MarioState state;
// 使用二维数组定义状态转换表
private static final MarioState[][] TRANSITION_TABLE = {
// SMALL, BIG, FIRE, DEAD
{MarioState.BIG, MarioState.SMALL, MarioState.SMALL, MarioState.DEAD}, // MUSHROOM
{MarioState.SMALL, MarioState.FIRE, MarioState.FIRE, MarioState.DEAD}, // FIRE_FLOWER
{MarioState.DEAD, MarioState.SMALL, MarioState.BIG, MarioState.DEAD}, // ENEMY_ATTACK
{MarioState.DEAD, MarioState.DEAD, MarioState.DEAD, MarioState.DEAD} // FALL_INTO_PIT
};
public Mario() {
state = MarioState.SMALL;
}
public void handleEvent(Event event) {
// 使用查表法获取状态转换后的新状态
MarioState newState = TRANSITION_TABLE[event.ordinal()][state.ordinal()];
// 打印状态转换信息
System.out.printf("从 %s 变为 %s%n", state, newState);
// 更新状态
state = newState;
}
}
public class MarioDemo {
public static void main(String[] args) {
Mario mario = new Mario();
mario.handleEvent(Event.MUSHROOM); // 变为大马里奥
mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥
mario.handleEvent(Event.FALL_INTO_PIT); // 变为死亡马里奥
}
}
使用了一个二维数组 TRANSITION_TABLE
来表示状态转换表。数组的行表示事件,列表示马里奥的当前状态,数组的元素表示新状态。通过查找表,可以直接获取状态转换后的新状态,从而简化状态机的实现。
handleEvent
方法中,根据事件和当前状态的序数来查找新状态,并更新马里奥的状态。这个查表法实现的有限状态机相比之前的面向对象实现更为简洁,但可能不适用于需要处理复杂事件或动作的场景。根据实际需求选择合适的实现方法是很重要的。
相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,只需要修改 transitionTable 和 actionTable 两个二维数组即可。把这两个二维数组存储在配置文件中,当需要修改状态机时,甚至可以不修改任何代码,只需要修改配置文件就可以了
状态模式
在查表法的代码实现中,事件触发的动作只是简单的状态或者数值,用一个 MarioState类型的二维数组 TRANSITION_TABLE 就能表示,二维数组中的值表示出发事件后的新状态。如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减分数、处理位置信息等等),就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性。
虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。实际上,针对分支逻辑法存在的问题,可以使用状态模式来解决。
**状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。**还是结合代码来理解这句话。
利用状态模式来补全 MarioStateMachine 类,补全后的代码如下所示。
以下是一个使用 Java 实现的简化版马里奥形态变化的案例代码,在代码添加了中文注释以便理解:
// 定义事件枚举类型
enum Event {
// 吃蘑菇,吃火花,遇到小怪,调入深坑
MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT
}
// 定义马里奥状态接口
interface MarioState {
void handleEvent(Event event);
}
// 实现死亡马里奥状态
class DeadMario implements MarioState {
private Mario mario;
public DeadMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
System.out.println("马里奥已死亡,无法处理事件");
}
}
// 实现小马里奥状态
class SmallMario implements MarioState {
private Mario mario;
public SmallMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
switch (event) {
case MUSHROOM:
System.out.println("变为大马里奥");
mario.setState(new BigMario(mario));
break;
case FIRE_FLOWER:
System.out.println("小马里奥不能直接变为火焰马里奥");
break;
case ENEMY_ATTACK:
System.out.println("小玛丽奥去世了");
mario.setState(new DeadMario(mario));
break;
case FALL_INTO_PIT:
System.out.println("小玛丽奥去世了");
mario.setState(new DeadMario(mario));
break;
}
}
}
// 实现大马里奥状态
class BigMario implements MarioState {
private Mario mario;
public BigMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
switch (event) {
case MUSHROOM:
System.out.println("保持大马里奥");
break;
case FIRE_FLOWER:
System.out.println("变为火焰马里奥");
mario.setState(new FireMario(mario));
break;
case ENEMY_ATTACK:
System.out.println("变为小马里奥");
mario.setState(new SmallMario(mario));
break;
case FALL_INTO_PIT:
System.out.println("马里奥去世了");
mario.setState(new DeadMario(mario));
break;
}
}
}
// 实现火焰马里奥状态
class FireMario implements MarioState {
private Mario mario;
public FireMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
switch (event) {
case MUSHROOM:
System.out.println("保持火焰马里奥");
break;
case FIRE_FLOWER:
System.out.println("保持火焰马里奥");
break;
case ENEMY_ATTACK:
System.out.println("变为大马里奥");
mario.setState(new BigMario(mario));
break;
case FALL_INTO_PIT:
System.out.println("马里奥去世了");
mario.setState(new DeadMario(mario));
break;
}
}
}
// 定义马里奥类,作为状态的上下文
class Mario {
private MarioState state;
public Mario() {
state = new SmallMario(this);
}
public void setState(MarioState state) {
this.state = state;
}
public void handleEvent(Event event) {
state.handleEvent(event);
}
}
// 测试类
public class MarioDemo {
public static void main(String[] args) {
Mario mario = new Mario();
mario.handleEvent(Event.MUSHROOM); // 变为大马里奥
mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥
mario.handleEvent(Event.ENEMY_ATTACK); // 变为大马里奥
}
}
在这个简化示例中,定义了 MarioState
接口以及实现了DeadMario
、 SmallMario
、BigMario
和 FireMario
类,分别表示马里奥的四种形态。每个形态类实现了 handleEvent
方法,用于处理不同的游戏事件并根据有限状态机规则进行状态转换。
Mario
类作为状态的上下文,用于管理和切换马里奥的状态。它有一个 setState
方法,用于更新当前状态。handleEvent
方法将事件传递给当前状态,以便根据事件执行相应的状态转换。
在 MarioDemo
测试类中,创建了一个 Mario
实例,并通过调用 handleEvent
方法模拟游戏中的事件。通过运行这个测试类,可以观察到马里奥根据有限状态机的规则在不同形态之间切换。