欢迎光临
我们一直在努力

初识状态模式

前言

在最近写项目的时候,涉及到一个需求,在途货物模块,货物的状态有四种,分别是待发,在途,到港,完成。这四个状态的关系是,由待发状态切换为在途状态时,需要增加字段比如装运日期字段,同时切换为在途状态后就不能切换为待发状态,其他的状态也是这个思想。起初写的时候按照以前的思路,需要切换什么状态,我就对应写一个方法,但是这样面临一个问题,判断的状态越来越多且写在一整个类中,太过于冗杂,查找起来较为困难,如果再增加多个状态就是非常灾难的。听潘老师的建议后,选择用状态模式来进行改写。

一、为什么需要状态模式?

🌰 如果你从网上买了一个东西,该订单的状态有以下状态

  • 待支付 (PendingPay)
  • 已支付 (Paid)

用户点击一个按钮 “下一步处理”,订单会按照状态顺序流转。

在没有使用状态模式时:

public class Order {

    private Byte status;

    // 定义状态常量
    public static final Byte PENDING_PAY = 1;
    public static final Byte PAID = 2;
    public static final Byte SHIPPED = 3;
    public static final Byte DELIVERED = 4;

    public Order() {
        this.status = PENDING_PAY; // 默认初始状态
    }

    public void nextStep() {
        if (PENDING_PAY.equals(status)) {
            System.out.println("用户付款成功");
            status = PAID;

        } else if (PAID.equals(status)) {
            System.out.println("商家发货");
            status = SHIPPED;
        }
    }
}

但是如果我再增加

  • 已发货 (Shipped)
  • 已签收 (Delivered)

那么就需要再写两个else if

public class Order {

    private Byte status;

    // 定义状态常量
    public static final Byte PENDING_PAY = 1;
    public static final Byte PAID = 2;
    public static final Byte SHIPPED = 3;
    public static final Byte DELIVERED = 4;

    public Order() {
        this.status = PENDING_PAY; // 默认初始状态
    }

    public void nextStep() {
        if (PENDING_PAY.equals(status)) {
            System.out.println("用户付款成功");
            status = PAID;

        } else if (PAID.equals(status)) {
            System.out.println("商家发货");
            status = SHIPPED;

        } else if (SHIPPED.equals(status)) {
            System.out.println("用户签收");
            status = DELIVERED;

        } else if (DELIVERED.equals(status)) {
            System.out.println("订单已完成");
        }
    }
}

在这里增加一个看不出来有什么麻烦的地方,假设我再增加状态 退货中 已退款 退货完成 等这些状态,那么增加的else if将是灾难级别的。

因此,状态模式可以很好的解决这样的问题。

二、什么是状态模式?

核心思想:允许一个对象在其内部状态改变时改变它的行为,使对象看起来好像修改了它的类。

状态模式有三个核心的角色:

角色 职责 类比现实世界
Context(上下文) 维护当前状态对象的引用,将客户端请求委托给当前状态,提供状态改变的接口 运动员:持有接力棒,跑到各个接力站
State(状态接口) 定义状态行为的接口,确保所有状态类有一致的方法 工作规范:定义接力站工作人员的工作标准
ConcreteState(具体状态) 实现当前状态下的具体行为,知道下一状态应该是什么,通过上下文接口改变状态 接力站工作人员:处理具体任务,告诉运动员下一站去哪

简单的解释:一个对象(称之为“上下文”)拥有多种状态。对于同一个请求,在不同的状态下,它会做出不同的响应。状态模式将每一个状态封装成一个独立的类,这样上下文对象就不需要维护一大堆 if-else 或 switch-case 来判断当前状态该做什么,而是将行为委托给代表当前状态的对象。

看起来还是有点难以理解,现在通过代码改造上面的订单状态来解释:

1. 对于同一个请求,在不同的状态下,它会做出不同的响应:

  • 当 status 为 待支付 时,nextStep() 执行付款逻辑
  • 当 status 为 已支付 时,nextStep() 执行发货逻辑
  • 当 status 为 已发货 时,nextStep() 执行签收逻辑
  • 当 status 为 已签收 时,订单已完成

2. 将每一个状态分装成一个独立的类:

public interface OrderState {
    void next(Order order);
}
// 状态1:待支付状态
public class PendingPayState implements OrderState {
  public void next(Order order) {
      System.out.println("用户付款成功");
      order.setStatus(Order.PAID);  // 状态改变:待支付状态 → 已支付状态 
  }
}

// 状态2:已支付状态  
public class PaidState implements OrderState {
    public void next(Order order) {
        System.out.println("商家发货");
        order.setStatus(Order.SHIPPED);  // 状态改变:已支付状态 → 已发货状态
    }
}

// 状态3:已发货状态
public class ShippedState implements OrderState {
    public void next(Order order) {
        System.out.println("用户签收");
        order.setStatus(Order.DELIVERED);  // 状态改变:已发货状态 → 已签收状态
    }
}

// 状态4:已签收状态
public class DeliveredState implements OrderState {
    public void next(Order order) {
        System.out.println("订单已完成");
    }
}

3. 上下文将行为委托给代表当前状态的对象:

用现实世界中的举个例子:

原始方式(if-else):

你接到任务时,要查手册:”我现在是什么状态?如果是状态1就做A,状态2就做B…”

状态模式:

你接到任务时,直接问当前的状态专员:”这个事情该怎么处理?”
待支付专员说:”我来处理付款”
已支付专员说:”我来处理发货”
已发货专员说:”我来处理签收”

public class Order {
    private OrderState currentState;  // 当前状态对象

    public void nextStep() {
        // 不再需要if-else,直接委托给当前状态对象
        currentState.next(this);  // this = 当前正在执行的 Order 对象
    }

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

currentState.next(this); 这块是比较难以理解的

以下是测试的结果:

订单状态: 待支付
当前状态: 待支付
Order对象(this): Order@279f2327
用户付款成功
下一步后状态: 已支付
当前状态: 已支付
Order对象(this): Order@279f2327
商家发货
下一步后状态: 已发货
当前状态: 已发货
Order对象(this): Order@279f2327
用户签收
下一步后状态: 已签收
当前状态: 已签收
Order对象(this): Order@279f2327
订单已完成
下一步后状态: 已签收

可以看到这个打印出的this都是Order@279f2327,大概可以猜测一下,我要改变这个order的状态,那么我首先就要知道修改的是哪个order,这个this意在指明告诉当前状态对象要操作Order@279f2327的订单

实例化一个对象后,调用nextStep()

Order order = new Order();
order.nextStep();

当执行nextStep()中的以下代码时:

currentState.next(this);

这个this(即新实例化的这个order对象本身)将会被传递

把 this 想象成你的身份证:
你去银行办事(调用 nextStep())
你把身份证递给柜员(传递 this)
柜员查看身份证,知道要操作哪个账户(状态对象知道操作哪个Order)
柜员办理业务并更新你的账户(状态对象调用 order.setState())

4. 对象看起来好像修改了这个类:

主方法测试:

public static void main(String[] args) {
    Order order = new Order();
    order.displayStatus();

    // 执行完整流程
    order.nextStep();  // 第一次:执行付款逻辑
    order.nextStep();  // 第二次:执行发货逻辑  
    order.nextStep();  // 第三次:执行签收逻辑
    order.nextStep();  // 第四次:执行完成逻辑
}

同一个 order.nextStep() 调用,每次执行不同的逻辑!

就好像 Order 类在运行时”变身”了一样:

  • 第一次调用时,它像是一个”待支付订单”
  • 第二次调用时,它像是一个”已支付订单”
  • 第三次调用时,它像是一个”已发货订单”
  • 第四次调用时,它像是一个”已完成订单”

三、在途货物模块该用状态模式

当前的需求图如下:

sequenceDiagram
    待发状态->>+在途状态: 增加 装运日期 预计到港日期 船名航次 等字段
    在途状态->>+到港状态: 增加 到港日期 字段
    到港状态->>+完成状态: 增加 完成日期 提单号 提单数 成本价(元/吨) 字段
            

由上图我们可以看到需要三次状态转换,按照原始的方法写:则需要3个逻辑,代码非常难找,同时由于状态的切换不可逆且无法跳跃,需要大量的判断当前状态。

以 到港 切换 完成 状态为例子:

public InTransitGoods switchToCompleted(Long id, InTransitGoodsDto.SwitchToCompletedRequest request) {
    InTransitGoods inTransitGoods = this.inTransitGoodsRepository.findById(id)
        .orElseThrow(EntityNotFoundException::new);

    // 校验当前货物的状态是否为[到港]
    if (!this.checkCurrentStatus(inTransitGoods, InTransitGoodsStatus.arrive.getValue())) {
        throw new IllegalArgumentException("当前货物状态不是[到港],无法切换至[完成]状态");
    }

    inTransitGoods.setStatus(InTransitGoodsStatus.completed.getValue());
    inTransitGoods.setCompletionDate(request.getCompletionDate());
    inTransitGoods.setLadingCount(request.getLadingCount());
    inTransitGoods.setLadingNumber(request.getLadingNumber());
    inTransitGoods.setCostPrice(request.getCostPrice());

    return this.inTransitGoodsRepository.save(inTransitGoods);
}

待发切换为在途,以及在途切换为完成也同理

也许三个不是很多,但是如果我有大量的状态,比如再增加 被滞留 退货中 退货完成 已完成 等状态,则可读性将会变得非常差。

改用状态模式:

/**
 * 在途货物状态接口
 */
public interface InTransitGoodsStatus {

    InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request);

    InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request);

    InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request);

}

这里以完成状态为例子

/**
 * 完成状态实现类
 */
public class CompletedStatus implements InTransitGoodsStatus {

    @Override
    public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
        throw new IllegalStateException("完成货物不能切换到在途");
    }

    @Override
    public InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request) {
        throw new IllegalStateException("完成货物不能切换到到港");
    }

    @Override
    public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
        throw new IllegalStateException("货物已经是完成状态");
    }

}

由于完成状态不存在处理逻辑,所以在此给出在途状态的示例:

public class InTransitState implements InTransitGoodsState {
    @Override
    public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
        throw new IllegalStateException("在途货物不能更新待发信息");
    }

    @Override
    public InTransitGoods switchToArrived(InTransitGoods inTransitGoods, InTransitGoodsDto.SwitchToArrivedRequest request) {
        inTransitGoods.setStatus(InTransitGoodsStatus.arrive.getValue());
        inTransitGoods.setArrivalDate(request.getArrivalDate());
        return inTransitGoods;
    }

    @Override
    public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
        throw new IllegalStateException("在途货物不能直接切换到完成");
    }
}

在这里增加一个统一创建和管理所有状态对象的类:

public class InTransitGoodsStatusFactory {
    private static final Map<Byte, InTransitGoodsState> statusMap = Map.of(
            InTransitGoodsStatus.pending.getValue(), new PendingStatus(),
            InTransitGoodsStatus.inTravel.getValue(), new InTransitStatus(),
            InTransitGoodsStatus.arrive.getValue(), new ArrivedStatus(),
            InTransitGoodsStatus.completed.getValue(), new CompletedStatus()
    );

    public static InTransitGoodsState getStatus(InTransitGoods inTransitGoods) {
        InTransitGoodsStatus inTransitGoodsStatus = statusMap.get(inTransitGoods.getStatus());
        if (inTransitGoodsStatus == null) throw new IllegalArgumentException("未知状态");
        return inTransitGoodsStatus;
    }
}

最终那一串的代码将会变成如下:

public InTransitGoods switchToCompleted(Long id, InTransitGoodsDto.SwitchToCompletedRequest request) {
    InTransitGoods inTransitGoods = this.inTransitGoodsRepository.findById(id).orElseThrow(EntityNotFoundException::new);
    
    InTransitGoodsStatus status = InTransitGoodsStateFactory.getStatus(inTransitGoods);
    inTransitGoods = status.switchToCompleted(inTransitGoods, request);

    return this.inTransitGoodsRepository.save(inTransitGoods);
}

这样,即使再增加多个状态时,也不用担心,增加几个状态我就重新创建几个状态类就可以了。

四、听潘老师讲解后的又一次启发

我们在写具体实现类的时候,以完成状态为例子,我们难免会怀疑这样写很麻烦,我每个实现类都需要去实现所有的方法,如果不满足则抛出异常,这样导致看起来十分冗余且不好分辨,证明还是没有深刻理解状态模式的巧妙之处。

/**
 * 完成状态实现类
 */
public class CompletedStatus implements InTransitGoodsStatus {

    @Override
    public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
        throw new IllegalStateException("完成货物不能切换到在途");
    }

    @Override
    public InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request) {
        throw new IllegalStateException("完成货物不能切换到到港");
    }

    @Override
    public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
        throw new IllegalStateException("货物已经是完成状态");
    }

}

而状态管理真正的巧妙之处在于:我可以写一个抽象类来实现这个接口:

先上图:

classDiagram
    class InTransitGoodsStatus {
        <<interface>>
        +switchToInTransit()
        +switchToArrived()
        +switchToCompleted()
    }
    
    InTransitGoodsStatus <|.. AbstractInTransitGoodsStatus
    class AbstractInTransitGoodsStatus {
        <<abstract>>
        +switchToInTransit()
        +switchToArrived()
        +switchToCompleted()
    }
    
    AbstractInTransitGoodsStatus <|-- PendingStatus
    AbstractInTransitGoodsStatus <|-- InTransitStatus
    AbstractInTransitGoodsStatus <|-- ArrivedStatus
    
    class PendingStatus {
        +switchToInTransit()
    }
    
    class InTransitStatus {
        +switchToArrived()
    }
    
    class ArrivedStatus {
        +switchToCompleted()
    }
            
public abstract class AbstractInTransitGoodsStatus implements InTransitGoodsState {
   
    @Override
    public InTransitGoods switchToInTransit(InTransitGoods goods, InTransitGoodsDto.SwitchToInTransitRequest request) {
        throw new IllegalStateException("完成货物不能切换到在途");
    }

    @Override
    public InTransitGoods switchToArrived(InTransitGoods goods, InTransitGoodsDto.SwitchToArrivedRequest request) {
        throw new IllegalStateException("完成货物不能切换到到港");
    }

    @Override
    public InTransitGoods switchToCompleted(InTransitGoods goods, InTransitGoodsDto.SwitchToCompletedRequest request) {
        throw new IllegalStateException("完成货物已经是完成状态");
    }
}

然后在对应的状态类中,只需要实现你所需要的逻辑转换即可:

public class InTransitStatus extends AbstractInTransitGoodsStatus {
    @Override
    public InTransitGoods switchToArrived(InTransitGoods inTransitGoods, InTransitGoodsDto.SwitchToArrivedRequest request) {
        inTransitGoods.setStatus(InTransitGoodsStatus.arrive.getValue());
        inTransitGoods.setArrivalDate(request.getArrivalDate());
        return inTransitGoods;
    }
}
sequenceDiagram
    待发状态->>+在途状态: 增加 装运日期 预计到港日期 船名航次 等字段
    在途状态->>+到港状态: 增加 到港日期 字段
    到港状态->>+完成状态: 增加 完成日期 提单号 提单数 成本价(元/吨) 字段
            

所以我们可以得出结论:
这个状态图完全可以和我们的代码一一对应,我在待发状态要实现在途状态的逻辑,那么只写转换成在途状态逻辑就好,如果增加了转换为其他状态,那么只需要再该状态对应的类下面再实现一个方法即可,如果没有该方法,抽象类会抛出异常。

结语

通过这次的项目,简单的了解了状态模式的基本原理,在学习状态模式时,了解到它与策略模式的类图相同,我会继续去学习策略模式,理解他们的异同。本文主要通过学习《Head First设计模式》有了大致的了解,如果存在理解错误的地方,欢迎指出!
非常感谢潘老师的教导和指导让我有了在这条路上有了走下去的勇气!!!

https://segmentfault.com/a/1190000047414736

未经允许不得转载:IT极限技术分享汇 » 初识状态模式

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址