XiaoLin's Blog

Xiao Lin

状态设计模式学习笔记

15
2024-02-26

状态设计模式

状态设计模式是一种行为设计模式,它允许一个对象在其内部状态改变时改变它的行为。这种模式通过把每个状态封装到一个类中,并且将行为的改变委托给代表当前状态的对象,来实现对象的行为随状态改变而改变。

目的

  • 封装基于状态的行为,并将行为委托到当前状态。
  • 减少条件语句(例如:if-else 或 switch-case)。
  • 方便新增或修改状态和其对应行为,增强可维护性和灵活性。

主要组件

  1. 环境(Context):维护一个指向当前状态对象的引用,并允许客户端请求触发状态转换。
  2. 抽象状态(State):定义所有具体状态共享的接口。环境通过这个接口调用具体状态定义的方法。
  3. 具体状态(Concrete States):实现抽象状态定义的接口。每个具体状态都提供了环境(Context)对特定于该状态的请求处理。

如何工作

  • 环境类包含一个指向当前状态对象的引用。
  • 环境类将客户端请求委托给当前存储的状态对象处理。
  • 当环境需要响应不同方式时,其内部所持有的当前状态对象会根据需要被其他具体状态对象所替换。

优点:

  1. 符合单一职责原则,每个州负责自己逻辑。
  2. 符合开闭原则,易于新增和修改 states。
  3. 将所有与某个特定州相关联的行为局部化到一个州中。

缺点:

  1. 如果 states 过多,则会导致系统中有大量类文件。
  2. 通信过多可能导致效率问题。

状态设计模式实现

我们有一个模拟一个简易的电视遥控器的需求,具有开启、关闭和调整音量的功能。
先来看看没有使用状态模式的简单demo代码

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

可以发现,当我们在执行开启关闭调节音量时,都需要使用大连的if-else代码去进行条件判断,如果我们的需求有更多的功能,如换台、快捷键等,条件分支会急速膨胀,所以此时状态设计模式就要登场了。

  1. 定义我们的抽象状态角色 TvState
package xyz.xiaolinz.demo.state.tv;

/**
 * 状态设计模式 - 抽象状态角色
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2024/02/26
 */
public interface TvState {

    /**
     * 打开
     *
     * @author huangmuhong
     * @date 2024/02/26
     * @since 1.0.0
     */
    void turnOn();

    /**
     * 关
     *
     * @author huangmuhong
     * @date 2024/02/26
     * @since 1.0.0
     */
    void turnOff();

    /**
     * 调节音量
     *
     * @param volume 体积
     * @author huangmuhong
     * @date 2024/02/26
     * @since 1.0.0
     */
    void adjustVolume(int volume);

}

我们将电视需求中,每一个需要对状态切换的功能都抽象到抽象状态接口之中。

  1. 我们为每个具体状态创建类,实现 TVState 接口。例如,我们创建 TVOnState 和 TVOffState 类
package xyz.xiaolinz.demo.state.tv;

/**
 * 状态设计模式 - 具体状态角色 - 只负责打开行为
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2024/02/26
 * @see TvState
 */
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);
    }
}


package xyz.xiaolinz.demo.state.tv;

/**
 * 状态设计模式 - 具体状态角色 - 只负责关闭行为
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2024/02/26
 * @see TvState
 */
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.");
    }
}
  1. 接下来定义上下文类 TvContext,这个类中主要维护了状态的切换和方法对外暴露等功能
package xyz.xiaolinz.demo.state.tv;

import java.util.Map;
import lombok.AllArgsConstructor;

/**
 * 状态设计模式 - 环境角色 - 维护状态
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2024/02/26
 */
public class TvContext {

    private final Map<TvStateEnum, TvState> stateMap =
        Map.of(TvStateEnum.ON, new TvOnState(), TvStateEnum.OFF, new TvOffState());

    private TvState tvState;

    /**
     * 设置电视状态
     *
     * @param type 类型
     * @author huangmuhong
     * @date 2024/02/26
     * @since 1.0.0
     */
    public void setTvState(TvStateEnum type) {
        this.tvState = stateMap.get(type);
    }

    public TvContext() {
        this.tvState = stateMap.get(TvStateEnum.OFF);
    }

    /**
     * 打开
     *
     * @author huangmuhong
     * @date 2024/02/26
     * @since 1.0.0
     */
    public void turnOn() {
        tvState.turnOn();
        tvState = stateMap.get(TvStateEnum.ON);
    }

    /**
     * 关
     *
     * @return void
     * @date 2024/02/26
     * @since 1.0.0
     */
    public void turnOff() {
        tvState.turnOff();
        tvState = stateMap.get(TvStateEnum.OFF);
    }

    /**
     * 调节音量
     *
     * @param volume 体积
     * @return void
     * @date 2024/02/26
     * @since 1.0.0
     */
    public void adjustVolume(int volume) {
        tvState.adjustVolume(volume);
    }

    /**
     * 电视状态枚举
     *
     * @author huangmuhong
     * @version 1.0.0
     * @date 2024/02/26
     * @see Enum
     */
    @AllArgsConstructor
    public static enum TvStateEnum {

        /**
         * 开
         */
        ON,

        /**
         * 关
         */
        OFF;

    }
}
  1. 执行调用,并查看结果
package xyz.xiaolinz.demo.state.tv;

/**
 * @author huangmuhong
 * @date 2024/2/26
 */
public class Main {
    public static void main(String[] args) {
        TvContext context = new TvContext();
        context.turnOn();
        context.turnOn();
        context.adjustVolume(10);
        context.turnOff();
        context.turnOff();
    }
}

有限状态机

有限状态机(Finite State Machine, FSM)是一种模型,它由一个有限数量的状态、状态之间的转换规则和可能的初始及终止状态组成。在任何时候,FSM都处于其中一个状态。根据某些条件或触发事件,FSM可以从一个状态转换到另一个状态。这种模型广泛应用于软件和硬件系统设计中,特别是在需要明确不同阶段或操作模式的系统中。

有限状态机的组件

  1. 状态(States):FSM可以处于的所有可能情况。每个状态都定义了对象在该特定条件下的行为。
  2. 转换(Transitions):从一个状态到另一个状态的过程。它由触发事件或条件决定。
  3. 事件(Events):触发转换的动作或现象。
  4. 动作(Actions):在进行某个特定转换时执行的活动。

应用场景

有限状态机在多种场合下都非常有用:

  • 游戏开发中管理角色或游戏环境的不同状态。
  • 网络协议(例如TCP协议)中管理连接的生命周期。
  • 用户界面逻辑控制,如按钮点击、滚动等交互行为管理。
  • 工作流管理,如订单处理流程。

状态设计模式与有限状态机

虽然“状态设计模式”和“有限状态机”之间存在一定关联性——二者都涉及到对象基于内部条件改变其行为——但它们侧重点不同。状态设计模式主要解决对象根据内部状态变化而改变其行为方式的问题,通常用于实现复杂对象态度上的灵活性和可扩展性;而有限状态机更加侧重于整体系统或模块在不同条件下如何从一种明确状况过渡到另一种明确状况,并对整个系统进行建模和分析。

对于刚刚给出的状态机的定义,我结合一个具体的例子,来进一步解释一下。

“超级马里奥”游戏不知道你玩过没有?在游戏中,马里奥可以变身为三种形态,小马里奥(Small Mario)、大马里奥(Big Mario)和火焰马里奥(Fire Mario)。马里奥可以通过吃蘑菇(Mushroom)、火花(Fire Flower)或被敌人攻击(Enemy Attack)来改变形态。我们将用状态图表示这个马里奥的有限状态机。

有限状态机的实现方式

状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。

分支逻辑法

分支逻辑法利用程序中的条件分支结构(如if-else语句、switch-case语句等)直接表达状态转换逻辑。每当有新的输入事件发生时,根据当前的状态和输入事件,通过分支逻辑选择合适的代码块执行,以实现从一个状态到另一个状态的转移。

  1. 定义状态和事件:首先明确有限状态机中包含哪些状态(State)和输入事件(Event)。通常可以使用枚举类型来定义它们。
  2. 初始化:设置有限状态机的初始状态。
  3. 处理输入:根据当前状态和输入事件,在条件分支结构中选择对应的处理逻辑。
  4. 转换并执行动作:执行对应于当前状态和输入事件所指定的动作,并根据需要更新到新的状态。

下面是一个使用switch语句实现的马里奥变化的代码示例

package xyz.xiaolinz.demo.state.mario.condition;

/**
 * @author huangmuhong
 * @date 2024/2/26
 */
public enum MarioState {
    SMALL, BIG, FIRE, DEAD;
}

package xyz.xiaolinz.demo.state.mario.condition;

/**
 * @author huangmuhong
 * @date 2024/2/26
 */
public enum MarioEvent {
    MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT;
}

package xyz.xiaolinz.demo.state.mario.condition;

import lombok.Getter;

/**
 * 马里奥
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2024/02/26
 */
public class Mario {

    @Getter
    private MarioState currentState;

    public Mario() {
        this.currentState = MarioState.SMALL;
    }

    public void handleEvent(MarioEvent event) {
        switch (currentState) {
            case SMALL:
                if (event.equals(MarioEvent.MUSHROOM)) {
                    currentState = MarioState.BIG;
                } else if (event.equals(MarioEvent.FIRE_FLOWER)) {
                    currentState = MarioState.FIRE;
                } else if (event.equals(MarioEvent.ENEMY_ATTACK)) {
                    currentState = MarioState.DEAD;
                } else if (event.equals(MarioEvent.FALL_INTO_PIT)) {
                    currentState = MarioState.DEAD;
                }
                break;
            case BIG:
                if (event.equals(MarioEvent.FIRE_FLOWER)) {
                    currentState = MarioState.FIRE;
                } else if (event.equals(MarioEvent.ENEMY_ATTACK)) {
                    currentState = MarioState.SMALL;
                } else if (event.equals(MarioEvent.FALL_INTO_PIT)) {
                    currentState = MarioState.DEAD;
                }
                break;
            case FIRE:
                if (event.equals(MarioEvent.ENEMY_ATTACK)) {
                    currentState = MarioState.SMALL;
                } else if (event.equals(MarioEvent.FALL_INTO_PIT)) {
                    currentState = MarioState.DEAD;
                }
                break;
            case DEAD:
            default:
                break;
        }
    }
}
  • 查看运行结果
package xyz.xiaolinz.demo.state.mario.condition;

/**
 * @author huangmuhong
 * @date 2024/2/26
 */
public class Main {
    public static void main(String[] args) {
        final Mario mario = new Mario();
        mario.handleEvent(MarioEvent.MUSHROOM);
        mario.handleEvent(MarioEvent.FIRE_FLOWER);
        mario.handleEvent(MarioEvent.ENEMY_ATTACK);
        mario.handleEvent(MarioEvent.FALL_INTO_PIT);
        
        System.out.println("mario = " + mario.getCurrentState());
    }
}

分支逻辑法是实现有限状态机较为简便直观的方法之一,特别适合于拥有少量简单明了转换规则与动作执行需求场景。然而,对于具备复杂变化规则与大量状态或者需要频繁修改维护规则时,则可能导致代码难以管理与扩展。

查表法

查表法是实现有限状态机(FSM)的一种高效且清晰的方法,特别适用于状态转换规则固定且相对简单的场景。在这种方法中,状态转换和相关动作通常通过查询预定义的表格来实现,而不是使用复杂的条件分支逻辑。这样,整个状态机的行为可以通过修改表格内容轻松调整,增强了代码的可读性和可维护性。

查表法主要涉及两种表:状态转换表动作执行表。状态转换表定义了在给定的当前状态和输入事件下应该转移到哪个新状态;动作执行表则指定了在特定状态转换发生时应该执行哪些操作。

  1. 定义枚举类型:首先定义表示所有可能状态(State)和输入事件(Event)的枚举类型。
  2. 创建查找表:创建两个二维数组或者映射(Map),一个用于存储从当前状态和事件到下一个状态的映射(即状态转换表),另一个存储对应于特定转换要执行的动作(即动作执行表)。
  3. 初始化查找表:根据FSM的设计,在程序初始化时填充这些查找表。每个条目都对应一个从当前状态加输入事件到下一个可能状态的映射,以及相应需要执行的动作。
  4. 处理输入事件:当FSM接收到输入事件时,根据当前状态和接收到的事件,在查找表中找到下一个状态以及需要执行的动作,并进行相应处理。

以下是使用查表法实现有限状态机逻辑简化版示例:

package xyz.xiaolinz.demo.state.mario.table;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import xyz.xiaolinz.demo.state.mario.MarioEvent;
import xyz.xiaolinz.demo.state.mario.MarioState;

/**
 * 马里奥状态事件对 - 存储状态转移表的键
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2024/02/26
 */
@Getter
@EqualsAndHashCode(callSuper = false)
public record MarioStateEventPair(MarioState state, MarioEvent event) {

}

package xyz.xiaolinz.demo.state.mario.table;

import java.util.Map;
import lombok.Getter;
import xyz.xiaolinz.demo.state.mario.MarioEvent;
import xyz.xiaolinz.demo.state.mario.MarioState;

/**
 * 有限状态机 - 查表法
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2024/02/27
 */
public class Mario {

    @Getter
    private MarioState currentState;

    private final Map<MarioStateEventPair, MarioState> transitionTable =
        Map.of(new MarioStateEventPair(MarioState.SMALL, MarioEvent.MUSHROOM), MarioState.BIG,
            new MarioStateEventPair(MarioState.SMALL, MarioEvent.FIRE_FLOWER), MarioState.FIRE,
            new MarioStateEventPair(MarioState.SMALL, MarioEvent.ENEMY_ATTACK), MarioState.DEAD,
            new MarioStateEventPair(MarioState.SMALL, MarioEvent.FALL_INTO_PIT), MarioState.DEAD,
            new MarioStateEventPair(MarioState.BIG, MarioEvent.FIRE_FLOWER), MarioState.FIRE,
            new MarioStateEventPair(MarioState.BIG, MarioEvent.ENEMY_ATTACK), MarioState.SMALL,
            new MarioStateEventPair(MarioState.BIG, MarioEvent.FALL_INTO_PIT), MarioState.DEAD,
            new MarioStateEventPair(MarioState.FIRE, MarioEvent.ENEMY_ATTACK), MarioState.SMALL,
            new MarioStateEventPair(MarioState.FIRE, MarioEvent.FALL_INTO_PIT), MarioState.DEAD);

    public Mario() {
        this.currentState = MarioState.SMALL;
    }

    public void handleEvent(MarioEvent event) {
        MarioStateEventPair pair = new MarioStateEventPair(currentState, event);
        currentState = transitionTable.getOrDefault(pair, currentState);
    }

}

在上述示例中,我们将MarioStateMarioEvent 包装成了 MarioStateEventPair,用于存储状态与事件的关联,并使用Map存储了状态鱼事件关联的状态转换关系,在调用handleEvent方法时,我们只需要从存储的表(Map)中取出我们的目标状态即可

结果:

package xyz.xiaolinz.demo.state.mario.table;

import xyz.xiaolinz.demo.state.mario.MarioEvent;

/**
 * @author huangmuhong
 * @date 2024/2/27
 */
public class Main {
    public static void main(String[] args) {
        final Mario mario = new Mario();
        mario.handleEvent(MarioEvent.MUSHROOM);
        mario.handleEvent(MarioEvent.FIRE_FLOWER);
        mario.handleEvent(MarioEvent.ENEMY_ATTACK);
        mario.handleEvent(MarioEvent.FALL_INTO_PIT);

        System.out.println("mario = " + mario.getCurrentState());
    }
}

  • 优点

    • 易于管理和扩展:只需修改或添加查找表条目即可轻松更新FSM逻辑。
    • 提高代码清晰度:减少复杂条件分支判断。
    • 方便调试:通过检视或修改查找表更直观地理解和调试FSM行为。
  • 缺点

    • 内存占用:较大或复杂FSM可能导致较大内存占用。
    • 动态变化难以处理:如果FSM逻辑频繁变化,则每次都需要更新查找表。

状态模式实现有限状态机

状态模式(State Pattern)是实现有限状态机的一种常见方式。它通过将每个状态封装为对象,并定义它们之间的转换规则来工作。这种模式下,上下文(Context)会持有一个表示当前状态的State对象,并且State对象可以自身决定在何种条件下进行到下一个状态。

@startuml  
class xyz.xiaolinz.demo.state.mario.state.MarioStateMachine {  
- MarioState currentState  
+ void eatMushroom()  
+ void eatFireFlower()  
+ void enemyAttack()  
+ void fallIntoPit()  
}  
  
  
class xyz.xiaolinz.demo.state.mario.state.MarioFireState {  
+ {static} MarioFireState INSTANCE  
+ void eatMushroom(MarioStateMachine)  
+ void eatFireFlower(MarioStateMachine)  
+ void enemyAttack(MarioStateMachine)  
+ void fallIntoPit(MarioStateMachine)  
}  
  
  
interface xyz.xiaolinz.demo.state.mario.state.MarioState {  
~ void eatMushroom(MarioStateMachine)  
~ void eatFireFlower(MarioStateMachine)  
~ void enemyAttack(MarioStateMachine)  
~ void fallIntoPit(MarioStateMachine)  
}  
  
class xyz.xiaolinz.demo.state.mario.state.MarioSmallState {  
+ {static} MarioSmallState INSTANCE  
+ void eatMushroom(MarioStateMachine)  
+ void eatFireFlower(MarioStateMachine)  
+ void enemyAttack(MarioStateMachine)  
+ void fallIntoPit(MarioStateMachine)  
}  
  
  
class xyz.xiaolinz.demo.state.mario.state.MarioDeadState {  
+ {static} MarioDeadState INSTANCE  
+ void eatMushroom(MarioStateMachine)  
+ void eatFireFlower(MarioStateMachine)  
+ void enemyAttack(MarioStateMachine)  
+ void fallIntoPit(MarioStateMachine)  
}  
  
  
class xyz.xiaolinz.demo.state.mario.state.MarioBigState {  
+ {static} MarioBigState INSTANCE  
+ void eatMushroom(MarioStateMachine)  
+ void eatFireFlower(MarioStateMachine)  
+ void enemyAttack(MarioStateMachine)  
+ void fallIntoPit(MarioStateMachine)  
}  
  
  
class xyz.xiaolinz.demo.state.mario.state.Main {  
+ {static} void main(String[])  
}  
  
  
  
xyz.xiaolinz.demo.state.mario.state.MarioState <|.. xyz.xiaolinz.demo.state.mario.state.MarioFireState  
xyz.xiaolinz.demo.state.mario.state.MarioState <|.. xyz.xiaolinz.demo.state.mario.state.MarioSmallState  
xyz.xiaolinz.demo.state.mario.state.MarioState <|.. xyz.xiaolinz.demo.state.mario.state.MarioDeadState  
xyz.xiaolinz.demo.state.mario.state.MarioState <|.. xyz.xiaolinz.demo.state.mario.state.MarioBigState  
@enduml