状态模式:状态驱动的行为变化

FreeGuideOnline 最新 2026-06-18

状态模式:状态驱动的行为变化

引言

对象的行为往往依赖于其内部状态。当状态改变时,对象可以自动切换其行为模式。状态模式(State Pattern) 就是一种面向对象的设计模式,它允许一个对象在其内部状态改变时改变它的行为,看起来就像是改变了其所属的类一样。本教程将带你理解什么是状态模式,为什么需要它,以及如何在实际项目中正确应用。

1. 问题引出:多分支条件语句的困境

想象你正在开发一款电子设备控制系统,例如文档编辑器自动售货机。这些系统有一个共同特点:系统行为随当前状态而变化。以简单的自动售货机为例,它可能有四种状态:无硬币状态、有硬币状态、售出商品状态、商品售罄状态。每个状态下,对用户的投币、退币、点击购买等操作的响应都不同。

public class GumballMachine {
    final static int NO_COIN = 0;
    final static int HAS_COIN = 1;
    final static int SOLD = 2;
    final static int SOLD_OUT = 3;
    int state = SOLD_OUT;
    int count = 0;

    public void insertCoin() {
        if (state == NO_COIN) {
            System.out.println("硬币已投入");
            state = HAS_COIN;
        } else if (state == HAS_COIN) {
            System.out.println("你已投入硬币,请勿重复投币");
        } else if (state == SOLD) {
            System.out.println("请稍后,正在出货");
        } else if (state == SOLD_OUT) {
            System.out.println("商品已售罄,无法投币");
        }
    }
    // 其他方法类似...
}

这种实现的痛点很明显:

  • 条件分支爆炸:每增加一个新状态,所有方法都需要修改 if-else 分支。
  • 可读性差:逻辑分散在大量条件判断中,难以维护。
  • 违反开闭原则:对扩展开放,对修改封闭?实际上我们对修改是开放的。
  • 状态转换逻辑混乱:状态码容易误用,转换条件散落在各处。

状态模式正是为了解决这类“由状态驱动行为”的系统设计问题

2. 状态模式的核心思想

将每种状态封装成一个独立的类,并将与状态相关的行为委托给该对象。上下文(Context)只需维护一个表示当前状态的状态对象,所有请求都转发给该状态对象。状态对象可以自行决定是否切换到另一个状态。

官方定义:

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

3. 模式结构

状态模式包含三个主要角色:

  • Context(上下文):持有当前状态对象的引用,并定义客户端关心的接口。它将所有状态相关的操作委托给当前状态对象处理。
  • State(状态接口):定义封装了与 Context 的一个特定状态相关的行为的接口。
  • ConcreteState(具体状态类):实现 State 接口,提供特定状态下的行为实现。如果操作使状态发生变化,具体状态类会设置 Context 的新状态对象。

状态模式结构图

(图示:Context 持有 State 引用,多个 ConcreteState 实现同一接口,Context 将行为委托给 State)

4. 代码实现:重构自动售货机

我们使用状态模式重新设计售货机。

步骤1:定义状态接口

public interface State {
    void insertCoin();
    void ejectCoin();
    void turnCrank();
    void dispense();
}

步骤2:实现具体的状态类

public class NoCoinState implements State {
    GumballMachine machine;

    public NoCoinState(GumballMachine machine) {
        this.machine = machine;
    }

    public void insertCoin() {
        System.out.println("硬币已投入");
        machine.setState(machine.getHasCoinState());
    }
    public void ejectCoin() { System.out.println("没有硬币可以退回"); }
    public void turnCrank() { System.out.println("请先投币"); }
    public void dispense() { System.out.println("需要先投币"); }
}

public class HasCoinState implements State {
    GumballMachine machine;
    public HasCoinState(GumballMachine machine) { this.machine = machine; }

    public void insertCoin() { System.out.println("你已投入硬币,请勿重复投币"); }
    public void ejectCoin() {
        System.out.println("硬币已退回");
        machine.setState(machine.getNoCoinState());
    }
    public void turnCrank() {
        System.out.println("转动曲柄,商品即将出货...");
        machine.setState(machine.getSoldState());
    }
    public void dispense() { System.out.println("无法直接取货,请先转动曲柄"); }
}

public class SoldState implements State {
    GumballMachine machine;
    public SoldState(GumballMachine machine) { this.machine = machine; }

    public void insertCoin() { System.out.println("请稍后,正在出货"); }
    public void ejectCoin() { System.out.println("你已经转动了曲柄,无法退币"); }
    public void turnCrank() { System.out.println("转动两次也没用"); }
    public void dispense() {
        machine.releaseBall();
        if (machine.getCount() > 0) {
            machine.setState(machine.getNoCoinState());
        } else {
            System.out.println("哦,商品卖完了!");
            machine.setState(machine.getSoldOutState());
        }
    }
}

public class SoldOutState implements State {
    GumballMachine machine;
    public SoldOutState(GumballMachine machine) { this.machine = machine; }
    public void insertCoin() { System.out.println("商品已售罄,无法投币"); }
    public void ejectCoin() { System.out.println("没有投入硬币,无法退币"); }
    public void turnCrank() { System.out.println("已售罄,转动曲柄无效"); }
    public void dispense() { System.out.println("没有商品可以发放"); }
}

步骤3:改造 Context(自动售货机)

public class GumballMachine {
    State noCoinState;
    State hasCoinState;
    State soldState;
    State soldOutState;

    State currentState;
    int count = 0;

    public GumballMachine(int initialCount) {
        noCoinState = new NoCoinState(this);
        hasCoinState = new HasCoinState(this);
        soldState = new SoldState(this);
        soldOutState = new SoldOutState(this);
        this.count = initialCount;
        if (initialCount > 0) {
            currentState = noCoinState;
        } else {
            currentState = soldOutState;
        }
    }

    public void insertCoin() { currentState.insertCoin(); }
    public void ejectCoin() { currentState.ejectCoin(); }
    public void turnCrank() { currentState.turnCrank(); currentState.dispense(); } // 注意这里每次转动后调用 dispense

    void setState(State state) { this.currentState = state; }
    void releaseBall() {
        System.out.println("一颗糖果滚出...");
        if (count > 0) count--;
    }
    // getters...
}

现在,售货机的行为完全由当前状态对象决定,新增状态只需一个新类,无需修改原有代码。

5. 状态模式 vs. 策略模式

两者类图非常相似,但意图不同:

特性 状态模式 策略模式
目的 根据内部状态改变行为,状态通常由自身或上下文驱动变化。 在可互换的算法族中选择一个,通常由客户端指定。
状态/策略认知 具体状态之间可能互相知晓并触发转换。 策略之间彼此独立,互不了解。
转换控制 Context 或具体状态类都可以主动改变状态。 客户端决定使用哪种策略,运行时不自动切换。
典型例子 TCP 连接状态(监听、已建立、关闭);订单状态流转。 排序算法选择、支付方式选择。

简单说:状态模式是“受状态驱动的策略模式”,状态会自行转移。

6. 优缺点分析

优点

  • 单一职责:将与特定状态相关的代码封装在独立类中。
  • 开闭原则:无需修改现有状态或 Context,就能引入新状态。
  • 消除庞大条件分支:让代码清晰无比。
  • 状态转换显式化:转换逻辑集中在状态类内部或上下文,易于追踪。

缺点

  • 类数量增加:每个状态都需要一个具体类,对于状态较少的系统可能显得小题大做。
  • 状态类可能了解上下文细节:如果状态类需要操作 Context 的复杂数据,会造成耦合。
  • 增加系统复杂性:简单的状态机用状态模式反而繁琐。

7. 实际应用场景

  1. 工作流引擎:请假审批流程(草稿→提交→审核中→通过/驳回)。
  2. 游戏开发:角色状态(站立、行走、攻击、死亡),每种状态响应不同按键输入。
  3. 网络连接:TCP 连接状态(CLOSED、LISTEN、SYN-SENT、ESTABLISHED 等),状态不同对数据的处理方式不同。
  4. 订单系统:待支付→已支付→已发货→已完成,每个状态允许的操作不同。
  5. 文档编辑器:只读模式、编辑模式、预览模式之间的切换。

8. 进阶:有状态的状态模式(状态拥有自己的状态)

有时状态类本身也会持有一些数据,比如售货机“有硬币状态”可能还要记录硬币面额。这样可以把更多行为内聚到状态对象中,使 Context 更简洁。但要注意生命周期管理,避免内存泄漏。

9. 总结

状态模式是处理“对象行为随状态改变”问题的利器。它通过将状态定义成独立的类,使复杂的条件判断逻辑变为多态调用,从而实现高内聚、低耦合的设计。当你面对一个充满 if-elseswitch 且状态会频繁切换的类时,不妨考虑引入状态模式。但也要权衡类的数量增加带来的复杂度,对于简单的状态机,使用查表法或枚举或许更合适。

选择工具的关键在于评估系统未来状态变化的可能性与复杂性——如果状态可能增长,且行为复杂,状态模式能帮你轻松驾驭变化。