状态模式详解:让对象随内部状态自动改变行为

一、什么是状态模式

定义(GOF) :允许一个对象在其内部状态改变时改变它的行为,对象看起来好像修改了它的类。

一句话理解:把"对象在不同状态下该做什么"封装成一组状态类 ,对象持有一个当前状态引用;状态一变,同样的方法调用就表现出不同行为------而且状态怎么转移、何时转移由状态自己决定,客户端不感知。


二、为什么需要状态模式(动机)

假设一个人一天的行为随"时间"变化:7 点吃早餐、12 点吃中餐、18 点吃晚餐。

不用状态模式(巨型 if-else):

java 复制代码
if (hour == 7) System.out.println("吃早餐");
else if (hour == 12) System.out.println("吃中餐");
else if (hour == 18) System.out.println("吃晚餐");

痛点:① 状态判断逻辑集中在一个超大条件表达式 里,随状态增多迅速膨胀、难以维护;② 新增状态要改这个方法,违反开闭原则;③ 行为与状态耦合在同一处。

用状态模式 :把每个状态(早晨/中午/傍晚)封装成 State 类,判断逻辑分散到各状态类里,状态之间自己决定"下一步去哪"。条件表达式消失,新增状态只需加类。


三、状态模式的结构

角色和职责

角色 职责
Context(上下文) 用户对象,持有当前 State 引用,对外提供统一方法(如 doSomething()),内部委托给当前状态
State(抽象状态) 封装与 Context 特定状态相关的行为(接口或抽象类)
ConcreteState(具体状态) 实现某个状态下的行为,并负责状态转移(必要时把 Context 切到下一个状态)

类结构图(mermaid)

#mermaid-svg-ArhEFBtokkJlIPKV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ArhEFBtokkJlIPKV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ArhEFBtokkJlIPKV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ArhEFBtokkJlIPKV .error-icon{fill:#552222;}#mermaid-svg-ArhEFBtokkJlIPKV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ArhEFBtokkJlIPKV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ArhEFBtokkJlIPKV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ArhEFBtokkJlIPKV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ArhEFBtokkJlIPKV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ArhEFBtokkJlIPKV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ArhEFBtokkJlIPKV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ArhEFBtokkJlIPKV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ArhEFBtokkJlIPKV .marker.cross{stroke:#333333;}#mermaid-svg-ArhEFBtokkJlIPKV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ArhEFBtokkJlIPKV p{margin:0;}#mermaid-svg-ArhEFBtokkJlIPKV g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-ArhEFBtokkJlIPKV g.classGroup text .title{font-weight:bolder;}#mermaid-svg-ArhEFBtokkJlIPKV .cluster-label text{fill:#333;}#mermaid-svg-ArhEFBtokkJlIPKV .cluster-label span{color:#333;}#mermaid-svg-ArhEFBtokkJlIPKV .cluster-label span p{background-color:transparent;}#mermaid-svg-ArhEFBtokkJlIPKV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ArhEFBtokkJlIPKV .cluster text{fill:#333;}#mermaid-svg-ArhEFBtokkJlIPKV .cluster span{color:#333;}#mermaid-svg-ArhEFBtokkJlIPKV .nodeLabel,#mermaid-svg-ArhEFBtokkJlIPKV .edgeLabel{color:#131300;}#mermaid-svg-ArhEFBtokkJlIPKV .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-ArhEFBtokkJlIPKV .label text{fill:#131300;}#mermaid-svg-ArhEFBtokkJlIPKV .labelBkg{background:#ECECFF;}#mermaid-svg-ArhEFBtokkJlIPKV .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-ArhEFBtokkJlIPKV .classTitle{font-weight:bolder;}#mermaid-svg-ArhEFBtokkJlIPKV .node rect,#mermaid-svg-ArhEFBtokkJlIPKV .node circle,#mermaid-svg-ArhEFBtokkJlIPKV .node ellipse,#mermaid-svg-ArhEFBtokkJlIPKV .node polygon,#mermaid-svg-ArhEFBtokkJlIPKV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ArhEFBtokkJlIPKV .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV g.clickable{cursor:pointer;}#mermaid-svg-ArhEFBtokkJlIPKV g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ArhEFBtokkJlIPKV g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-ArhEFBtokkJlIPKV .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-ArhEFBtokkJlIPKV .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ArhEFBtokkJlIPKV .dashed-line{stroke-dasharray:3;}#mermaid-svg-ArhEFBtokkJlIPKV .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-ArhEFBtokkJlIPKV #compositionStart,#mermaid-svg-ArhEFBtokkJlIPKV .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #compositionEnd,#mermaid-svg-ArhEFBtokkJlIPKV .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #dependencyStart,#mermaid-svg-ArhEFBtokkJlIPKV .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #dependencyStart,#mermaid-svg-ArhEFBtokkJlIPKV .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #extensionStart,#mermaid-svg-ArhEFBtokkJlIPKV .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #extensionEnd,#mermaid-svg-ArhEFBtokkJlIPKV .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #aggregationStart,#mermaid-svg-ArhEFBtokkJlIPKV .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #aggregationEnd,#mermaid-svg-ArhEFBtokkJlIPKV .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #lollipopStart,#mermaid-svg-ArhEFBtokkJlIPKV .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV #lollipopEnd,#mermaid-svg-ArhEFBtokkJlIPKV .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ArhEFBtokkJlIPKV .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-ArhEFBtokkJlIPKV .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ArhEFBtokkJlIPKV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ArhEFBtokkJlIPKV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ArhEFBtokkJlIPKV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 持有当前状态
转移
转移
<<abstract>>
State
+doSomething(Person)
MState
LState
DState
Person
-int hour
-State state
+doSomething()
+setState(State)
+getHour() : int


四、代码举例

java 复制代码
// ========== 1. 上下文:持有当前状态 ==========
public class Person {
    private int hour;
    private State state;

    public Person() {
        state = new MState();   // 初始状态:早晨
    }
    public int getHour() { return hour; }
    public void setHour(int hour) { this.hour = hour; }
    public State getState() { return state; }
    public void setState(State state) { this.state = state; }

    // 纯委托:把行为交给当前状态,自己不写任何 if-else
    public void doSomething() {
        state.doSomething(this);
    }
}

// ========== 2. 抽象状态 ==========
public abstract class State {
    public abstract void doSomething(Person person);
}

// ========== 3. 具体状态:早晨 ==========
public class MState extends State {
    @Override
    public void doSomething(Person person) {
        if (person.getHour() == 7) {
            System.out.println("吃早餐");
        } else {
            // 不匹配就转移,并把请求交给下一个状态
            person.setState(new LState());
            person.doSomething();
        }
    }
}

// ========== 3. 具体状态:中午 ==========
public class LState extends State {
    @Override
    public void doSomething(Person person) {
        if (person.getHour() == 12) {
            System.out.println("吃中餐");
        } else {
            person.setState(new DState());
            person.doSomething();
        }
    }
}

// ========== 3. 具体状态:傍晚(终态) ==========
public class DState extends State {
    @Override
    public void doSomething(Person person) {
        if (person.getHour() == 18) {
            System.out.println("吃晚餐");
        } else {
            System.out.println("时间未定义");
        }
    }
}

// ========== 4. 测试 ==========
public class MainClass {
    public static void main(String[] args) {
        Person person = new Person();

        person.setHour(7);
        person.doSomething();      // 吃早餐

        person.setHour(12);
        person.doSomething();      // 吃中餐

        person.setHour(18);
        person.doSomething();      // 吃晚餐

        person.setHour(20);
        person.doSomething();      // 时间未定义
    }
}

运行结果

复制代码
吃早餐
吃中餐
吃晚餐
时间未定义

注意:上面的"链式匹配"示例适合演示"从初始状态找匹配行为"的场景。它的状态转移是沿链单向 的(MState→LState→DState)。若你的业务需要状态长期驻留(如订单今天付了款、明天才发货),应采用下面的"状态驻留写法"。


五、进阶:更地道的状态驻留写法(订单状态机)

真正工程里,状态模式的价值是状态长期驻留 + 由事件触发转移 。以一个订单为例:新建 → 已支付 → 已发货 → 已完成,每个状态下能做的操作不同,且状态由 pay()/ship() 等事件驱动转移:

java 复制代码
// 抽象状态:每个事件方法默认拒绝,由具体状态重写
public interface OrderState {
    void pay(Order order);
    void ship(Order order);
    String name();
}
// 新建状态:能支付,支付后转"已支付"
public class NewState implements OrderState {
    public void pay(Order o)  { System.out.println("支付成功"); o.setState(new PaidState()); }
    public void ship(Order o) { System.out.println("错误:未支付不能发货"); }
    public String name() { return "NEW"; }
}
// 已支付状态:能发货,发货后转"已发货"
public class PaidState implements OrderState {
    public void pay(Order o)  { System.out.println("已支付,无需重复支付"); }
    public void ship(Order o) { System.out.println("发货成功"); o.setState(new ShippedState()); }
    public String name() { return "PAID"; }
}
// 已发货状态:支付/发货都不再响应(或转完成)
public class ShippedState implements OrderState {
    public void pay(Order o)  { System.out.println("已发货,无需支付"); }
    public void ship(Order o) { System.out.println("已发货,无需重复发货"); }
    public String name() { return "SHIPPED"; }
}
public class Order {
    private OrderState state = new NewState();
    public void setState(OrderState s) { this.state = s; }
    public void pay()  { state.pay(this); }
    public void ship() { state.ship(this); }
    public String state() { return state.name(); }
}
java 复制代码
Order o = new Order();
o.pay();   // 支付成功(NEW→PAID)
o.ship();  // 发货成功(PAID→SHIPPED)
o.pay();   // 已发货,无需支付

这种写法里:状态会驻留 (payment 后停在 PAID),转移由状态自己决定 (NewState 里 o.setState(new PaidState())),客户端只调用 pay()/ship() 业务事件,完全不感知当前是哪个状态------这才是状态模式最典型的应用形态。


六、状态模式 vs 策略模式(本质区别)

上篇《策略模式》提过二者结构极像(都持有对象、都委托调用),但意图完全不同:

维度 状态模式 策略模式
谁决定用哪个 对象内部状态自动转移,客户端不感知"在换算法" 客户端显式传入/set 一个策略
客户端是否感知切换 不感知(只知道调业务方法) 感知(自己选了策略)
转移发生在哪 状态类内部 setState(...) 自包含 由客户端/Context 外部决定
核心意图 同一对象随状态自动变行为 同一件事换不同做法
状态是否驻留 是(状态长期存在) 否(算法可随时换,无所谓"驻留")

记忆口诀:策略是"我(客户端)选打法",状态是"我(对象)随处境变打法"。


七、真实世界中的状态模式(JDK / 框架)

  • java.lang.Thread.State:NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED。线程对象的行为(能否运行、是否阻塞)随状态而变------状态模式概念的最直观体现(虽其内部未必用 State 类实现,但是该模式的教科书级案例)。
  • Servlet 生命周期 :init → service → destroy,以及 HttpSession 的创建/活跃/过期状态。
  • 订单 / 工单状态机:待支付→已支付→已发货→已完成;待处理→处理中→已关闭。业务里无处不在。
  • Spring Statemachine / Activiti 工作流:用专门的引擎管理状态与转移,是状态模式的企业级落地。
  • TCP 连接状态:CLOSED / LISTEN / ESTABLISHED / TIME_WAIT 等,连接对象行为随状态切换。

八、应用场景与优缺点

适用场景

  • 对象行为依赖它的状态 ,且运行时状态会频繁变化。
  • 存在大量与状态相关的 if-else / switch 分支,且随状态增多难以维护。
  • 状态转移规则清晰,希望把"判断逻辑"分散到各状态类里。

优点

  1. 消除巨型条件分支 :状态相关行为分散到各 ConcreteState,代码清爽。
  2. 符合开闭原则 :新增状态只需加一个 ConcreteState 类,改一处转移即可。
  3. 状态转移显式化 :用类与 setState 把"状态机"表达得比 if-else 清晰得多,也便于加日志/校验。

缺点

  1. 类数量增加 :每个状态一个类,状态多时类膨胀(可结合享元共享无状态实例)。
  2. 状态转移分散 :转移散落在各状态类里,全局"状态图"不如一张集中表直观(复杂场景建议用专门的状态机引擎统一管理)。

九、总结

一句话 :状态模式 = 把对象在不同状态下的行为封装成一组状态类,状态自己决定"做什么 + 转去哪",让对象随内部状态自动改变行为。

记忆口诀:

条件分支太臃肿,拆成状态各管各;

行为随境自己变,转移自包含其中。

相关推荐
需要8262 小时前
Spring Boot 启动流程:run() 里到底发生了什么
java·spring boot·后端
云絮.2 小时前
Mybatis(一)
java·数据库·mybatis
希洛兔3 小时前
里氏替换原则详解:子类必须能透明替换基类(含继承风险演示 + 正确重构 + JDK 案例)
java·里氏替换原则
kevien_G13 小时前
微服务之OpenFeign最佳实践
java·微服务·架构
江湖有缘3 小时前
Docker实战 | 使用Docker部署ShipShipShip自托管产品更新与功能投票系统
java·linux·服务器·docker
范什么特西3 小时前
亲和路由11
java
诺NA亚4 小时前
ElasticSearch+RAG初使用
java
SamDeepThinking4 小时前
synchronized 和 lock 有什么区别?
java·后端·面试
m0_587383004 小时前
无人自助拼豆系统的全栈开发方案
java·后端·struts·小程序·架构