每天认识一个设计模式-状态模式:优雅化解对象行为的千面之变

每天认识一个设计模式-状态模式:优雅化解对象行为的千面之变_第1张图片

一、前言:当订单状态成为代码的梦魇

在当今数字化时代,电商购物已经成为人们生活中不可或缺的一部分。我们在享受便捷购物体验的同时,可能很少会想到电商系统背后复杂的订单状态管理。想象一下,你在电商平台上下单购买了一件心仪已久的商品,从点击 “提交订单” 的那一刻起,这个订单就开始了它在系统中的 “奇幻之旅”。​

起初,订单处于 “待支付” 状态,这是交易的起点。当你完成支付后,订单状态变为 “已支付”,接着进入 “待发货” 阶段,此时商家开始准备商品并安排发货。随后,订单进入 “运输中” 状态,直到你收到商品并确认收货,订单才最终变为 “已完成” 状态。这看似简单的几个状态转换,背后却隐藏着复杂的业务逻辑和代码实现。​

以一个中等规模的电商系统为例,随着业务的不断发展和用户需求的日益多样化,订单状态从最初简单的 “待支付→已支付→已完成” 逐渐演变为包含 “待审核→审核中→已取消→退款中→退款完成→部分发货→缺货处理” 等十几种甚至更多的状态。每新增一个状态,代码中就不得不新增大量的if - else或switch - case分支,用来处理不同状态下的业务逻辑。例如,在处理订单支付逻辑时,代码可能如下所示:

public class OrderService {
    public void processPayment(Order order) {
        switch (order.getStatus()) {
            case "待支付":
                // 处理支付逻辑,调用支付接口,更新订单状态等
                if (paymentGateway.pay(order.getAmount())) {
                    order.setStatus("已支付");
                    // 其他相关操作,如记录支付日志,通知商家等
                } else {
                    // 支付失败处理
                    order.setStatus("支付失败");
                }
                break;
            case "已取消":
                System.out.println("该订单已取消,无法支付");
                break;
            case "退款中":
                System.out.println("该订单正在退款中,无法支付");
                break;
            // 其他状态的处理
            default:
                System.out.println("订单状态异常,无法支付");
        }
    }
}

随着订单状态的不断增加,这段代码会变得越来越冗长和复杂,维护成本也会急剧飙升。每修改或新增一个状态,都需要在多个if - else或switch - case分支中进行相应的调整,稍有不慎就可能引发各种难以排查的bug。而且,这些状态转换规则散落在代码的各个角落,使得代码的可读性和可维护性极差。​

更糟糕的是,当业务需求发生变化,比如需要在某个特定状态下增加新的业务逻辑时,往往需要对大量的代码进行修改,这无疑增加了开发的风险和难度。这种情况下,传统的条件判断方式已经难以满足系统的可维护性和扩展性要求。​

那么,有没有一种更好的解决方案呢?答案就是状态模式(State Pattern)。状态模式能够将复杂的状态相关行为进行解耦,让代码变得更加清晰、易于维护,并且符合开闭原则,为我们构建优雅、可扩展的系统提供了有力的支持。接下来,就让我们深入探索状态模式的奥秘吧!

二、 状态模式的模式定义与核心思想

在状态模式(State Pattern)中,类的行为是基于它的状态改变的,这种类型的设计模式属于行为型模式。在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

状态模式允许对象在内部状态改变时改变其行为,使得对象在不同的状态下有不同的行为表现。通过将每个状态封装成独立的类,可以避免使用大量的条件语句来实现状态切换。

2.1 标准定义解析​

GoF 对状态模式的定义是:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。这听起来可能有点抽象,让我们来深入剖析一下。​

在日常生活中,我们可以找到很多状态模式的例子。以交通信号灯为例,它有红灯、绿灯和黄灯三种状态。在不同的状态下,交通信号灯的行为是不同的。当处于红灯状态时,它会指示车辆和行人停止前进;当变为绿灯时,就允许车辆和行人通行;而黄灯亮起时,则是提醒大家即将切换状态,做好准备。这里的交通信号灯就是一个对象,它的行为(指示停止、通行或提醒)随着其内部状态(红灯、绿灯、黄灯)的改变而改变。​

在软件开发中,状态模式同样发挥着重要作用。假设我们正在开发一个游戏,游戏中的角色有不同的状态,比如站立、行走、奔跑和跳跃。每个状态下,角色的行为和动画表现都不一样。如果不使用状态模式,我们可能会在角色类中编写大量的条件判断语句来处理不同状态下的行为,这会使代码变得冗长且难以维护。而使用状态模式,我们可以将每个状态下的行为封装到独立的状态类中,角色类只需要维护当前的状态对象,并在需要时调用其相应的方法即可。这样,当角色的状态发生改变时,其行为也会自动切换,代码结构更加清晰,可维护性大大提高。​

2.2 有限状态机(FSM)理论基础​

状态模式与有限状态机(Finite State Machine,FSM)理论紧密相关。有限状态机是一种抽象的计算模型,它由以下几个关键要素构成:​

  1. 状态(State):系统在某一时刻所处的状况,比如电商订单的 “待支付”“已支付”“已发货” 等状态,或者游戏角色的 “站立”“行走”“奔跑” 等状态。每个状态都代表了系统的一种特定情况,并且具有相应的行为和属性。​
  2. 事件(Event):能够触发状态转换的动作或条件。用户点击 “支付” 按钮就是一个事件,它会触发订单从 “待支付” 状态转换到 “已支付” 状态;
  3. 转换(Transition):定义了从一个状态到另一个状态的迁移规则。当特定的事件发生时,系统会根据转换规则从当前状态切换到下一个状态。当用户完成支付操作(事件)后,根据转换规则,订单状态从 “待支付” 转换为 “已支付”;
  4. 动作(Action):在状态转换时执行的具体操作。当订单状态从 “待支付” 变为 “已支付” 时,系统可能会执行发送支付成功通知、更新库存等动作;

通过将状态逻辑抽象为状态机模型,可以清晰地描述系统的行为轨迹。我们可以使用状态转移图来直观地展示有限状态机的各个状态、事件、转换和动作之间的关系。在状态转移图中,每个状态用一个节点表示,事件用有向边表示,边上标注事件名称和可能的动作,状态转换则通过有向边的指向来体现。这样,我们可以一目了然地看到系统在不同状态之间的转换路径以及在转换过程中执行的动作,有助于我们更好地理解和设计系统的状态管理逻辑。

2.3 设计目标

在软件开发中,我们经常会使用条件分支语句(如if - else或switch - case)来处理不同状态下的业务逻辑。然而,随着系统中状态数量的增加和业务逻辑的复杂性提高,这种传统的条件分支方式会暴露出许多问题,就像代码中散发出来的 “坏味道”,让人难以忍受。​

传统条件分支存在以下问题:​

⚠️违反开闭原则:开闭原则是面向对象设计中的一个重要原则,它要求软件实体(类、模块、函数等)对扩展开放,对修改封闭。当我们使用传统条件分支来处理状态逻辑时,每新增一个状态,都需要在原有的条件判断代码中添加新的分支。这就意味着我们需要修改大量已有的代码,违反了开闭原则。

在电商订单支付逻辑中,如果要新增一个 “待审核” 状态,就需要在processPayment方法的switch语句中添加一个新的case分支,这不仅增加了代码修改的风险,也使得代码的维护变得更加困难。​

⚠️职责分散:在传统条件分支中,状态逻辑与业务逻辑紧密耦合在一起。不同状态下的行为逻辑都写在同一个方法中,导致这个方法承担了过多的职责。这使得代码的可读性和可维护性变差,因为我们需要在大量的条件判断代码中寻找和理解特定状态下的业务逻辑。

在处理订单状态的代码中,可能会同时包含支付处理、发货处理、退款处理等多种业务逻辑,并且这些逻辑都通过条件判断分散在不同的分支中,使得代码结构混乱,难以理清各个部分的职责。​

⚠️可读性差:复杂的条件分支嵌套会使代码变得难以理解。当状态之间的转换规则复杂,并且存在多个层次的条件判断时,代码会变得像一团乱麻,让人望而生畏。

在一个具有多种状态和复杂业务规则的系统中,可能会出现多层嵌套的if - else语句,每个if语句都有自己的条件和对应的处理逻辑,这使得代码的阅读和调试变得异常困难,开发人员需要花费大量的时间和精力来理解代码的执行逻辑。​

状态模式通过将每个状态的行为封装为独立类,巧妙地解决了上述问题,实现了以下优势:​

单一职责:每个状态类专注于自身行为,只负责处理特定状态下的业务逻辑。这样,每个状态类的职责明确,代码结构清晰,易于理解和维护。

可扩展性:新增状态只需添加新类,无需修改原有逻辑。当业务需求发生变化,需要新增一个状态时,我们只需要创建一个新的状态类,实现相应的行为方法,然后在需要的地方进行状态切换即可。这样,我们可以在不修改原有代码的基础上,轻松地扩展系统的功能,符合开闭原则。

行为分离:状态转换与业务逻辑解耦,使得系统的状态管理和业务逻辑更加清晰。状态模式将状态转换的逻辑封装在状态类中,而不是分散在业务逻辑代码中。这样,业务逻辑代码只需要关注业务本身,而不需要关心状态的具体转换细节。

 三、状态模式的模式原型设计与 UML 解析

 3.1 状态模式的核心 UML 结构

状态模式的核心 UML 结构包含三个关键部分:Context(上下文)、State(抽象状态)和ConcreteState(具体状态),以及它们之间的关系。通过以下 PlantUML 代码,我们可以清晰地构建出状态模式的 UML 类图。

每天认识一个设计模式-状态模式:优雅化解对象行为的千面之变_第2张图片

Context类与State接口之间通过关联关系连接,这意味着Context持有一个State类型的引用,用于表示当前的状态。而State接口与ConcreteStateA、ConcreteStateB等具体状态类之间是继承关系,具体状态类实现了State接口中定义的行为方法。

因此在状态模式中,它的核心角色主要有三个:

  • 上下文(Context):定义了客户感兴趣的接口,并维护一个当前状态对象的引用。上下文可以通过状态对象来委托处理状态相关的行为。

  • 状态(State):定义了一个接口,用于封装与上下文相关的一个状态的行为。

  • 具体状态(Concrete State):实现了状态接口,负责处理与该状态相关的行为。具体状态对象通常会在内部维护一个对上下文对象的引用,以便根据不同的条件切换到不同的状态。

这种结构设计使得状态模式具有良好的扩展性和维护性,当需要添加新的状态时,只需要创建一个新的具体状态类并实现State接口即可,而不需要修改Context类和其他已有的状态类。 

3.2状态模式的状态转换策略 

状态转换是状态模式中的关键环节,它决定了对象在不同状态之间的切换方式。在状态模式中,有两种常见的状态转换实现方式:Context 控制和 State 自主转移。 下面咱们可以通过一个示例来进行说明,假设我们有一个简单的电梯控制系统,电梯有三种状态:关门、运行和开门。​

首先定义状态接口和具体状态类:

// 状态接口
interface ElevatorState {
    void handle(Context context);
}

// 关门状态类
class DoorClosedState implements ElevatorState {
    @Override
    public void handle(Context context) {
        System.out.println("电梯门已关闭");
        // 模拟一些条件,比如接收到运行指令
        if (shouldMove()) {
            context.setState(new MovingState());
        }
    }

    private boolean shouldMove() {
        // 实际应用中这里是具体的判断逻辑,比如有乘客按下楼层按钮等
        return true;
    }
}

// 运行状态类
class MovingState implements ElevatorState {
    @Override
    public void handle(Context context) {
        System.out.println("电梯正在运行");
        // 模拟到达目标楼层
        if (hasArrived()) {
            context.setState(new DoorOpenState());
        }
    }

    private boolean hasArrived() {
        // 实际应用中这里是具体的判断逻辑,比如到达设定楼层
        return true;
    }
}

// 开门状态类
class DoorOpenState implements ElevatorState {
    @Override
    public void handle(Context context) {
        System.out.println("电梯门已打开");
        // 模拟一些条件,比如等待一段时间后关门
        if (shouldClose()) {
            context.setState(new DoorClosedState());
        }
    }

    private boolean shouldClose() {
        // 实际应用中这里是具体的判断逻辑,比如达到开门时间
        return true;
    }
}

然后定义上下文类:

class Context {
    private ElevatorState state;

    public Context() {
        // 初始状态为关门
        this.state = new DoorClosedState();
    }

    public void setState(ElevatorState state) {
        this.state = state;
    }

    public void request() {
        state.handle(this);
    }
}

 Context 控制

在这种方式下,上下文类决定状态转换逻辑。比如我们在上下文类中添加一个方法来控制状态转换:

class Context {
    // 省略其他代码

    // Context控制状态转换
    public void controlStateTransition(String event) {
        if (event.equals("startMoving") && state instanceof DoorClosedState) {
            setState(new MovingState());
        } else if (event.equals("arrive") && state instanceof MovingState) {
            setState(new DoorOpenState());
        } else if (event.equals("closeDoor") && state instanceof DoorOpenState) {
            setState(new DoorClosedState());
        }
    }
}

 使用时:

public class Main {
    public static void main(String[] args) {
        Context context = new Context();
        context.request(); // 输出:电梯门已关闭

        context.controlStateTransition("startMoving");
        context.request(); // 输出:电梯正在运行

        context.controlStateTransition("arrive");
        context.request(); // 输出:电梯门已打开

        context.controlStateTransition("closeDoor");
        context.request(); // 输出:电梯门已关闭
    }
}

这种方式的优点是状态转换逻辑集中在上下文类中,易于理解和维护,适合状态转换规则比较简单、集中的场景。但是,随着业务的复杂,上下文类可能会变得臃肿,承担过多的职责。​

State 自主转移

状态类内部处理转换逻辑。我们之前定义的具体状态类DoorClosedState、MovingState和DoorOpenState中已经体现了这种方式,比如DoorClosedState类中在handle方法里根据shouldMove方法的判断结果自行决定是否转换到MovingState状态。

class DoorClosedState implements ElevatorState {
    @Override
    public void handle(Context context) {
        System.out.println("电梯门已关闭");
        if (shouldMove()) {
            context.setState(new MovingState());
        }
    }

    private boolean shouldMove() {
        return true;
    }
}

 使用时:

public class Main {
    public static void main(String[] args) {
        Context context = new Context();
        context.request(); // 输出:电梯门已关闭

        context.request(); // 由于shouldMove返回true,状态自动转换为MovingState,输出:电梯正在运行

        context.request(); // 由于hasArrived返回true,状态自动转换为DoorOpenState,输出:电梯门已打开

        context.request(); // 由于shouldClose返回true,状态自动转换为DoorClosedState,输出:电梯门已关闭
    }
}

这种方式的优点是状态类的职责更加单一,每个状态类只负责自己状态下的行为和状态转换逻辑,符合单一职责原则。而且,当状态转换规则发生变化时,只需要修改相应的状态类,不会影响到其他类,具有更好的扩展性。但是,状态转换逻辑分散在各个状态类中,可能会增加代码的理解难度,特别是当状态类较多时,状态转换的整体流程不太容易把握。

四、状态模式的适用场景与框架实践 

4.1 典型应用场景​

存在 3 个以上状态的条件判断:在许多业务场景中,对象的状态数量往往较多。

以电商订单为例,除了常见的待支付、已支付、已发货、已完成等状态,还可能涉及到待审核(如某些特殊商品或促销活动下的订单需要人工审核)、部分发货(商品分批次发货的情况)、退货中、换货中、退款完成等状态。

当状态数量达到 3 个以上时,使用传统的if - else或switch - case语句来处理状态相关的业务逻辑,会使代码变得冗长且难以维护。而状态模式可以将每个状态的行为封装成独立的类,使得代码结构更加清晰,易于扩展和维护。

比如在处理订单发货逻辑时,如果使用状态模式,“已支付” 状态类中只需要实现已支付状态下的发货逻辑,当订单状态变为 “已支付” 时,直接调用该状态类的发货方法即可,无需在大量的条件判断中寻找和执行相应的逻辑。​

状态转换规则明确的业务场景:在一些业务场景中,状态之间的转换规则是明确且固定的。

比如在一个工作流管理系统中,任务的状态可能包括待处理、处理中、已完成、已驳回等。任务从待处理状态转换到处理中状态,通常是因为分配了处理人员并开始处理;从处理中状态转换到已完成状态,是因为任务处理完毕且通过审核;从已完成状态转换到已驳回状态,可能是因为审核不通过。在这种状态转换规则明确的场景下,使用状态模式可以将状态转换逻辑封装在状态类中,使得状态转换的过程更加清晰和可控。而且,当业务规则发生变化时,只需要修改相应状态类中的转换逻辑,而不会影响到其他部分的代码。

当工作流系统中增加了新的审核规则,导致从处理中状态转换到已完成状态的条件发生变化时,只需要在 “处理中” 状态类中修改转换逻辑,而不会对其他状态类和业务逻辑造成影响。​

需要历史状态追溯的系统:在一些对数据完整性和操作可追溯性要求较高的系统中,如财务系统、医疗信息系统、物流追踪系统等,需要记录对象的历史状态。

以物流追踪系统为例,包裹的状态会随着运输过程不断变化,从仓库出库、运输中、到达中转站、派送中、已签收等。通过使用状态模式,我们可以在每个状态转换时,记录下当前的状态信息以及转换的时间、原因等相关数据。这样,当需要查询包裹的运输历史时,就可以根据记录的历史状态信息,清晰地了解到包裹在每个阶段的状态和变化情况。而且,状态模式使得状态的管理更加集中和规范,便于进行历史状态的记录和查询操作。

在财务系统中,每一笔交易的状态变化(如订单创建、支付成功、退款申请、退款完成等)都可以通过状态模式进行记录和管理,方便后续的财务审计和业务分析。

4.2状态模式设计思想在开源框架中的应用 

Spring State Machine​

Spring State Machine 是 Spring 生态系统中用于构建状态机的强大框架,它通过注解和配置的方式实现状态机的管理,为开发者提供了一系列丰富的功能,这也是典型状态模式应用的框架之一。

分层状态结构:Spring State Machine 支持定义分层状态,这对于复杂的业务场景非常有用。通过StateMachineConfigurerAdapter类来配置状态机,使用withStates方法定义状态机状态,parent属性指定分层状态关系。

 

import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.state.State;

import java.util.EnumSet;

@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter {

    public static enum OrderStates {
        NORMAL_FLOW, ABNORMAL_FLOW,
        PENDING_PAYMENT, PAID, SHIPPED, COMPLETED,
        CANCELED, REFUNDING, RETURNING
    }

    public static enum OrderEvents {
        PAY, SHIP, COMPLETE, CANCEL, REFUND, RETURN
    }

    @Override
    public void configure(StateMachineStateConfigurer states) throws Exception {
        states
               .withStates()
                // 顶层正常流程状态
               .state(NORMAL_FLOW)
                   .subStates(EnumSet.of(PENDING_PAYMENT, PAID, SHIPPED, COMPLETED))
                   .initial(PENDING_PAYMENT)
                // 顶层异常流程状态
               .state(ABNORMAL_FLOW)
                   .subStates(EnumSet.of(CANCELED, REFUNDING, RETURNING));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
        transitions
               .withExternal()
                   .source(PENDING_PAYMENT).target(PAID).event(PAY)
                   .and()
               .withExternal()
                   .source(PAID).target(SHIPPED).event(SHIP)
                   .and()
               .withExternal()
                   .source(SHIPPED).target(COMPLETED).event(COMPLETE)
                   .and()
               .withExternal()
                   .source(PENDING_PAYMENT).target(CANCELED).event(CANCEL)
                   .and()
               .withExternal()
                   .source(PAID).target(REFUNDING).event(REFUND)
                   .and()
               .withExternal()
                   .source(SHIPPED).target(RETURNING).event(RETURN);
    }
}

这里我们定义了OrderStates枚举表示订单状态,OrderEvents枚举表示订单事件。通过configure方法配置状态机状态,将NORMAL_FLOW和ABNORMAL_FLOW作为顶层状态,PENDING_PAYMENT、PAID、SHIPPED、COMPLETED作为NORMAL_FLOW的子状态,CANCELED、REFUNDING、RETURNING作为ABNORMAL_FLOW的子状态。这种分层结构使复杂的状态关系更清晰,易于代码实现和维护。

事件驱动的状态转换:在 Spring State Machine 中,状态的转换是由事件驱动的。在上述配置类中,通过withExternal方法配置状态转换,source指定源状态,target指定目标状态,event指定触发转换的事件。

transitions
   .withExternal()
       .source(PENDING_PAYMENT).target(PAID).event(PAY)
       .and()
   .withExternal()
       .source(PAID).target(SHIPPED).event(SHIP)
   .and()
   .withExternal()
       .source(SHIPPED).target(COMPLETED).event(COMPLETE);

这段代码表示当PAY事件发生时,状态从PENDING_PAYMENT转换到PAID;SHIP事件发生时,状态从PAID转换到SHIPPED;COMPLETE事件发生时,状态从SHIPPED转换到COMPLETED。同时,Spring State Machine 支持在状态转换过程中执行自定义操作,通过实现StateMachineListener接口,重写transition方法,在状态转换时执行自定义逻辑。

import org.springframework.messaging.Message;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.State;
import org.springframework.stereotype.Component;

@Component
public class OrderStateMachineListener extends StateMachineListenerAdapter {

    @Override
    public void transition(StateMachine stateMachine, Message message, State source, State target) {
        if (source.getStateId() == OrderStates.PAID && target.getStateId() == OrderStates.SHIPPED) {
            // 发送发货通知给用户的逻辑
            System.out.println("发送发货通知给用户");
        }
    }
}

这样在订单状态从PAID转换到SHIPPED时,打印 “发送发货通知给用户”,实际应用中可替换为真实的通知逻辑,实现灵活的状态管理和系统交互。

状态持久化与恢复:Spring State Machine 提供了状态持久化的功能,可以将状态机的当前状态保存到数据库或其他持久化存储中。使用JdbcStateMachinePersister类将状态机状态持久化到数据库。首先配置数据源和持久化器: 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.statemachine.persist.JdbcStateMachinePersister;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class StateMachinePersistenceConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
               .setType(EmbeddedDatabaseType.H2)
               .addScript("classpath:org/springframework/statemachine/persist/schema.sql")
               .build();
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public JdbcStateMachinePersister jdbcStateMachinePersister(DataSource dataSource) {
        return new JdbcStateMachinePersister<>(new OrderStateMachineRepository(), dataSource);
    }
}

 然后在状态机配置类中注入持久化器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.persist.StateMachinePersister;

import java.util.EnumSet;

@Configuration
@EnableStateMachine
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter {

    @Autowired
    private StateMachinePersister persister;

    // 状态和事件枚举定义...

    @Override
    public void configure(StateMachineStateConfigurer states) throws Exception {
        // 状态配置...
    }

    @Override
    public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
        // 转换配置...
    }
}

在系统启动时,通过持久化器从数据库读取状态机状态,恢复状态机到之前的状态,保证系统可靠性和数据一致性。在分布式电商系统中,订单状态机状态分布在多个节点,状态持久化和恢复功能确保节点故障或系统重启时,订单状态的一致性和完整性,避免业务错误。

Java 线程状态机

Java 线程的生命周期(NEW→RUNNABLE→BLOCKED→WAITING→TIMED_WAITING→TERMINATED)是状态模式的经典实现。在 Java 中,Thread类通过内部的状态变量和相关方法来管理线程的状态转换。下面结合Thread类的实际源码来看看状态模式的体现过程:

状态定义

​在Thread类中,定义了线程的各种状态,这些状态就对应状态模式中的不同状态。在Thread类中有如下关于状态的定义:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

这里的每个枚举值代表线程的一种状态,就像状态模式中定义的不同状态。

NEW 状态:

Thread thread = new Thread(() -> {
    System.out.println("Thread is running");
});

此时thread.getState()返回的就是Thread.State.NEW ,这个状态下线程没有任何实际执行的行为,仅仅是一个对象的创建。​

当线程处于NEW状态时,它还没有开始执行,只是一个刚被创建的线程对象。在源码层面,创建一个Thread对象时进入RUNNABLE 状态。

RUNNABLE 状态:

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}
private native void start0();

start()方法首先进行一些合法性检查,然后调用本地方法start0(),这个方法会将线程的状态转变为RUNNABLE,此时线程就具备了被 JVM 调度执行的资格。当调用start()方法后,线程进入RUNNABLE状态,表示它可以被 JVM 调度执行。

BLOCKED 状态:

public class BlockedExample {
    private static final Object lock = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此时thread1和thread2都尝试获取lock对象的锁,当thread2先获取到锁并进入同步代码块时,thread1就会进入BLOCKED状态,直到thread2释放锁。

在RUNNABLE状态下,如果线程试图获取一个被其他线程持有的锁,它会进入BLOCKED状态,等待获取锁。

public final void wait() throws InterruptedException {
    wait(0);
}

 它最终调用的wait(long timeout)方法会将当前线程的状态转变为WAITING。​如果线程调用了wait()方法,它会进入WAITING状态,等待其他线程的通知。例如上面例子中thread1调用了lock.wait(),此时thread1的状态就会变为WAITING,并且会释放它持有的lock锁,暂停执行,直到收到notify()或notifyAll()通知。

状态转换

start()方法实现状态转换:​如上述start()方法源码所示,start()方法会将线程从NEW状态转换到RUNNABLE状态,这个过程涉及到线程的初始化、资源分配等操作。​

wait()方法实现状态转换:​wait()方法会将线程从RUNNABLE状态转换到WAITING状态,并且会释放线程持有的锁,这是通过 JVM 的底层机制来实现的。在Object类的wait方法中,JVM 会处理线程状态的转变以及锁的释放等操作,使得线程进入等待状态。​

通过这种方式,Java 线程状态机实现了对线程生命周期的有效管理,并且符合状态模式的设计思想,将不同状态下的行为封装在对应的状态处理逻辑中,使得线程的状态管理更加清晰和可靠。在多线程编程中,我们可以根据线程的不同状态来进行相应的操作,比如在BLOCKED状态下,可以等待一段时间后重新尝试获取锁,或者进行其他的资源准备工作。

 五、总结

状态模式将状态转化路径显式化,通过将不同状态下的行为封装到独立的状态类中,使得状态之间的转换规则一目了然,我们能够清晰地看到系统在不同状态之间的流转过程,这极大地提高了代码的可读性和可维护性。

同时,状态模式严格遵循单一职责原则,每个状态类只专注于自身状态下的行为实现,避免了一个类承担过多的职责,使得代码结构更加清晰,各个部分的职责明确,降低了代码的耦合度,便于后续的扩展和修改。

此外,状态模式还显著提升了系统的可观测性,由于状态的变更被封装在状态类中,并且状态类可以记录状态变更的相关信息,使得我们能够方便地对状态变更进行追溯和监控,及时发现系统中可能出现的问题,为系统的稳定运行提供了有力保障。 但是我们在实际应用也需要注意正确的使用方式:

⚠️状态爆炸预防:随着业务的不断发展,系统中的状态数量可能会迅速增加,导致 “状态爆炸” 的问题。为了预防状态爆炸,可以采用状态分层和合并的策略。状态分层可以将复杂的状态关系进行梳理,将相关的状态归为一组,形成层次结构,这样可以减少状态的数量和复杂度。

⚠️与策略模式的抉择:状态模式和策略模式在结构上有一些相似之处,但它们的侧重点不同。状态模式主要侧重于对象状态相关的行为,不同的状态会导致对象行为的变化;而策略模式侧重于算法的替换,它允许在运行时选择不同的算法来完成某个任务。

所以在实际开发中,我们需要根据具体的业务需求来选择使用哪种模式。如果业务逻辑主要围绕对象的状态变化展开,并且不同状态下的行为差异较大,那么状态模式是一个更好的选择;如果业务需求是在不同的场景下选择不同的算法来实现某个功能,那么策略模式更为合适。

⚠️DDD 状态建模:在领域驱动设计(DDD)中,将状态作为领域对象进行处理可以更好地体现业务逻辑和领域知识。通过 DDD 状态建模,可以将状态与业务规则、事件等紧密结合,提高系统的业务表达能力和可维护性。

你可能感兴趣的:(设计模式,设计模式,状态模式)