状态模式:让对象随着状态起舞

状态模式,说白了就是让一个对象在内部状态变化时,自动切换自己的行为。你想想看,就像一个人——开心的时候会笑,难过的时候会哭,生气的时候会吼。同一个人,状态不同,行为完全不同。

我记得刚工作那会儿,接手过一个订单系统。订单有各种状态:待支付、已支付、已发货、已完成、已取消。每个状态下,用户能做的操作都不一样。比如待支付可以取消,已支付就不能取消了。当时代码写得那叫一个痛苦——满屏的 if-else,改一个状态就要动好几个地方。

后来我学了状态模式,才明白:把每个状态封装成独立的类,让状态自己决定行为。这才是正道。

核心思想

状态模式的核心就一句话:将状态相关的行为封装到独立的状态类中。上下文对象不再自己判断状态,而是把行为委托给当前的状态对象。

这样做的好处很明显:

  • 消除条件分支——不用再写大段的 if-else 或 switch-case
  • 状态转换清晰——每个状态类只关心自己的转换逻辑
  • 易于扩展——加一个新状态,加一个新类就行,不用改现有代码

关键点:状态模式让对象看起来像是改变了它的类。因为行为完全变了,外部调用者感觉就像换了一个对象。

结构图

先看一张图,理解状态模式的整体结构:

状态模式结构图 Context - state: State + request(): void State (接口) + handle(context): void ConcreteStateA + handle(context): void ConcreteStateB + handle(context): void ConcreteStateC + handle(context): void 状态之间可以互相转换,由每个具体状态的 handle() 方法决定下一个状态

代码示例

拿一个简单的播放器来说。播放器有三种状态:播放中、暂停中、停止中。每种状态下,点击播放按钮的行为都不一样。

// 状态接口
interface PlayerState {
    play(player: MediaPlayer): void;
    pause(player: MediaPlayer): void;
    stop(player: MediaPlayer): void;
}

// 播放中状态
class PlayingState implements PlayerState {
    play(player: MediaPlayer): void {
        console.log("已经在播放了,别点了");
    }
    pause(player: MediaPlayer): void {
        console.log("暂停播放");
        player.setState(new PausedState());
    }
    stop(player: MediaPlayer): void {
        console.log("停止播放");
        player.setState(new StoppedState());
    }
}

// 暂停中状态
class PausedState implements PlayerState {
    play(player: MediaPlayer): void {
        console.log("继续播放");
        player.setState(new PlayingState());
    }
    pause(player: MediaPlayer): void {
        console.log("已经是暂停状态了");
    }
    stop(player: MediaPlayer): void {
        console.log("停止播放");
        player.setState(new StoppedState());
    }
}

// 停止中状态
class StoppedState implements PlayerState {
    play(player: MediaPlayer): void {
        console.log("从头开始播放");
        player.setState(new PlayingState());
    }
    pause(player: MediaPlayer): void {
        console.log("没在播放,不能暂停");
    }
    stop(player: MediaPlayer): void {
        console.log("已经是停止状态了");
    }
}

// 上下文类
class MediaPlayer {
    private state: PlayerState;

    constructor() {
        this.state = new StoppedState();
    }

    setState(state: PlayerState): void {
        this.state = state;
    }

    play(): void {
        this.state.play(this);
    }

    pause(): void {
        this.state.pause(this);
    }

    stop(): void {
        this.state.stop(this);
    }
}

// 使用示例
const player = new MediaPlayer();
player.play();   // 输出:从头开始播放
player.pause();  // 输出:暂停播放
player.play();   // 输出:继续播放
player.stop();   // 输出:停止播放
player.pause();  // 输出:没在播放,不能暂停

我的习惯:写状态模式时,我会先画一张状态转换图。把每个状态能做什么、能转到什么状态都列清楚。代码写起来就顺了。

状态转换表

为了方便理解,我把播放器的状态转换整理成了一张表:

当前状态 操作 结果行为 下一个状态
停止中 播放 从头开始播放 播放中
停止中 暂停 没在播放,不能暂停 停止中
停止中 停止 已经是停止状态 停止中
播放中 播放 已经在播放了 播放中
播放中 暂停 暂停播放 暂停中
播放中 停止 停止播放 停止中
暂停中 播放 继续播放 播放中
暂停中 暂停 已经是暂停状态 暂停中
暂停中 停止 停止播放 停止中

适用场景

状态模式不是万能的,它适合以下情况:

  • 对象的行为依赖于它的状态,并且状态会变化
  • 状态数量不多,但每个状态下的行为逻辑比较复杂
  • 状态转换规则明确,可以画成状态图
  • 你想避免大量的条件分支,让代码更干净

我曾经踩过的坑:有一次我把状态模式用在了状态数量超过20个的场景上。结果状态类爆炸,维护起来比 if-else 还痛苦。后来我改用状态表+策略模式才搞定。记住:状态模式适合状态数量可控的场景,一般不超过10个。

与策略模式的区别

很多人会把状态模式和策略模式搞混。我简单说一下:

  • 策略模式:客户端主动选择策略,策略之间是平级的,互不依赖
  • 状态模式:状态自动转换,状态之间有关联,一个状态知道下一个状态是什么

说白了,策略模式是「我想用哪个用哪个」,状态模式是「到了什么状态就做什么事」。

实际项目中的应用

我在项目中用过状态模式的地方:

  • 订单状态机:待支付→已支付→已发货→已完成,每个状态下的操作不同
  • 工作流引擎:审批流程中的待审批→审批中→已通过/已驳回
  • 游戏角色:角色的站立、行走、奔跑、跳跃状态,每个状态下的动画和物理行为不同
  • 网络连接:已连接、连接中、已断开、重连中

嗯,这里要注意一点:状态模式虽然好,但不要滥用。如果你的状态只有两三个,而且每个状态下的行为很简单,那用 if-else 反而更直接。设计模式是为了解决问题,不是为了炫技。

总结一下:状态模式把状态相关的行为封装到独立的状态类中,让对象在内部状态改变时自动切换行为。它消除了条件分支,让代码更清晰、更易扩展。但要注意控制状态数量,别让状态类泛滥成灾。

我个人习惯在项目初期就设计好状态转换图。哪怕后面需求变了,改状态图比改满屏的 if-else 要轻松得多。你想想看,是不是这个道理?