跳转到内容

状态模式 (State)

一个对象在不同状态下能干的事不一样:草稿能提交、已提交能审批、已通过就不能再改。如果在主类里用一堆 if (state == DRAFT) ... else if (state == SUBMITTED) ...,每加一个状态或一种操作,这段分支就会再长一截,也很难单测某一种状态的行为。

状态模式的做法是:把「当前是什么状态」和「这个状态下怎么处理请求」交给一个状态对象,主类(上下文)只持有一个当前状态的引用,收到请求时转给这个状态去处理;状态处理完需要切到下一状态时,再通过上下文把当前状态换掉。这样每种状态的行为和转换都收在各自类里,主类保持薄一层,扩展新状态主要是加新类而不是改老分支。


很多业务里,同一个对象在不同阶段能执行的操作不同:例如工单在草稿时只能保存和提交,提交后只能审批或退回,通过后就只读。最直接的写法是在处理「提交」「审批」的入口处,根据当前状态写一大串 if-else:if (state == DRAFT) { 允许提交; 改状态; } else if (state == SUBMITTED) { ... }。这样会有几个问题:所有状态逻辑挤在一处,改一个状态要在一大块代码里找;加一个新状态要改多处分支;某一种状态的行为也不好单独测。

更稳妥的做法是:每种状态对应一个类,类里只写「在这个状态下能做什么、做完切到哪」。主类(上下文)不关心具体是哪个状态,只持有一个「当前状态」的引用,请求来了就调当前状态的 handle(context);状态类在需要时调用 context.setState(nextState) 完成切换。这样「状态」和「在该状态下的行为 + 转移」都封装在状态类里,主类只负责转发和持有一个 state 引用。


二、上下文、状态接口与具体状态

Section titled “二、上下文、状态接口与具体状态”
  • 上下文 (Context):业务上的「主体」(例如工单、任务、连接),持有一个当前状态 (State) 的引用;对外提供业务方法(如 submit()approve()),内部把这些调用转给当前状态对象处理。同时提供 setState(State s),供状态类在适当时机把当前状态换成下一个。
  • 抽象状态 (State):定义「处理请求」的接口,例如 handle(Context ctx) 或按操作拆成 submit(Context ctx)approve(Context ctx)。方法里能拿到 Context,以便在逻辑里调用 ctx.setState(nextState) 完成状态转换。
  • 具体状态 (ConcreteState):实现上述接口,每个类对应一种状态(如草稿、已提交、已通过)。在方法里实现「当前状态下允不允许做这件事、做了之后切到哪个状态」;不允许时可以抛异常或返回错误,允许则执行业务并调用 context.setState(...)

客户端(或上层服务)只和 Context 打交道,不直接 new 具体状态;状态之间的切换由状态类在 handle 里根据业务决定,避免在主类里写满分支。


上下文持有当前状态,请求委托给状态;状态可依赖上下文做切换。

classDiagram
    class Context {
        -state State
        +request() void
        +setState(State s) void
    }
    class State {
        <<interface>>
        +handle(context)* void
    }
    class ConcreteStateA {
        +handle(context) void
    }
    class ConcreteStateB {
        +handle(context) void
    }
    Context o-- State
    State <|.. ConcreteStateA
    State <|.. ConcreteStateB

不依赖框架,用接口和两个具体状态把「委托 + 切换」跑通。

public interface State {
void handle(Context context);
}
public class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle(this);
}
}
public class ConcreteStateA implements State {
@Override
public void handle(Context context) {
System.out.println("StateA 处理,切换到 StateB");
context.setState(new ConcreteStateB());
}
}
public class ConcreteStateB implements State {
@Override
public void handle(Context context) {
System.out.println("StateB 处理");
}
}
Context ctx = new Context();
ctx.setState(new ConcreteStateA());
ctx.request(); // StateA 处理,切换到 StateB
ctx.request(); // StateB 处理

下面把「上下文」换成工单、「状态」换成草稿/已提交/已通过/已驳回,用 Bean 注入各状态,工单在处理操作时委托当前状态并完成转换。


后台里常见「工单」「申请单」一类的东西:有固定几个状态(草稿、已提交、已通过、已驳回等),每个状态下允许的操作不同,操作后可能切到下一状态。用状态模式可以把「当前状态下能不能做、做了切到哪」都放进对应的状态类,工单本身只持有一个当前状态引用并转发请求。

这里用四种状态:草稿 (DRAFT)、已提交 (SUBMITTED)、已通过 (APPROVED)、已驳回 (REJECTED)。草稿可提交→已提交;已提交可通过→已通过、驳回→已驳回;已通过/已驳回不再做状态转换,只返回结果。每个状态是一个 Bean,工单(Context)由服务层创建或加载,处理动作时委托给当前状态。

工单需要存当前状态,以及业务数据;状态枚举或字符串均可,这里用枚举便于和状态类对应。

package com.example.demo.state;
import lombok.Data;
import javax.persistence.*;
import java.time.Instant;
@Data
@Entity
@Table(name = "work_order")
public class WorkOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@Enumerated(EnumType.STRING)
private WorkOrderStatus status;
private Instant createdAt;
private Instant updatedAt;
public enum WorkOrderStatus { DRAFT, SUBMITTED, APPROVED, REJECTED }
}
package com.example.demo.state;
import org.springframework.data.jpa.repository.JpaRepository;
public interface WorkOrderRepository extends JpaRepository<WorkOrder, Long> {}

状态类需要「处理某种操作」并可能切换状态,这里用「操作类型 + 上下文」作为参数,方便不同操作共用一个入口;你也可以按操作拆成 submit(WorkOrderContext ctx)approve(WorkOrderContext ctx)

package com.example.demo.state;
public interface WorkOrderState {
/** 处理操作,返回是否允许;内部可调用 ctx.setState(...) 完成转换 */
void handle(WorkOrderContext ctx, String action);
}
package com.example.demo.state;
public interface WorkOrderContext {
WorkOrder getOrder();
void setState(WorkOrderState state);
void save(); // 持久化当前状态等
}

上下文实现类持有一个 WorkOrder 和当前 WorkOrderStatesetState 时更新实体里的 status 并保存;处理动作时把请求转给当前状态。

package com.example.demo.state;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Map;
@Component
@Scope("prototype")
@RequiredArgsConstructor
public class WorkOrderContextImpl implements WorkOrderContext {
private final WorkOrderRepository workOrderRepository;
private final Map<WorkOrder.WorkOrderStatus, WorkOrderState> stateRegistry;
private WorkOrder order;
private WorkOrderState state;
public void load(WorkOrder order) {
this.order = order;
this.state = stateRegistry.get(order.getStatus());
}
@Override
public WorkOrder getOrder() { return order; }
@Override
public void setState(WorkOrderState state) {
this.state = state;
this.order.setStatus(state.toStatus());
this.order.setUpdatedAt(Instant.now());
save();
}
@Override
public void save() {
workOrderRepository.save(order);
}
public void perform(String action) {
state.handle(this, action);
}
}

上下文用 @Scope("prototype"),每次从容器取到的是新实例,避免并发下共用同一个 order/state。stateRegistry 即下面配置的 Map,根据 order.getStatus() 取到当前状态 Bean。

5.3 具体状态:草稿、已提交、已通过、已驳回

Section titled “5.3 具体状态:草稿、已提交、已通过、已驳回”

每个状态类只处理「当前状态下允许的操作」和「做完切到哪」。不允许的操作直接抛异常或返回错误信息。

package com.example.demo.state;
import org.springframework.stereotype.Component;
@Component
public class DraftState implements WorkOrderState {
@Override
public void handle(WorkOrderContext ctx, String action) {
if ("submit".equals(action)) {
ctx.setState(/* 已提交状态 Bean */);
return;
}
throw new IllegalStateException("DRAFT only allows submit");
}
public WorkOrder.WorkOrderStatus toStatus() { return WorkOrder.WorkOrderStatus.DRAFT; }
}

为减少重复,下面用「状态类里持有下一状态的引用」的方式写一版,下一状态通过构造或 Setter 注入;也可以改用 Registry 按枚举取 Bean。

package com.example.demo.state;
import org.springframework.stereotype.Component;
@Component
public class DraftState implements WorkOrderState {
private final SubmittedState submittedState;
public DraftState(SubmittedState submittedState) {
this.submittedState = submittedState;
}
@Override
public void handle(WorkOrderContext ctx, String action) {
if (!"submit".equals(action)) {
throw new IllegalStateException("DRAFT only allows submit, got: " + action);
}
ctx.setState(submittedState);
}
public WorkOrder.WorkOrderStatus toStatus() { return WorkOrder.WorkOrderStatus.DRAFT; }
}
package com.example.demo.state;
import org.springframework.stereotype.Component;
@Component
public class SubmittedState implements WorkOrderState {
private final ApprovedState approvedState;
private final RejectedState rejectedState;
public SubmittedState(ApprovedState approvedState, RejectedState rejectedState) {
this.approvedState = approvedState;
this.rejectedState = rejectedState;
}
@Override
public void handle(WorkOrderContext ctx, String action) {
if ("approve".equals(action)) {
ctx.setState(approvedState);
return;
}
if ("reject".equals(action)) {
ctx.setState(rejectedState);
return;
}
throw new IllegalStateException("SUBMITTED only allows approve/reject, got: " + action);
}
public WorkOrder.WorkOrderStatus toStatus() { return WorkOrder.WorkOrderStatus.SUBMITTED; }
}
package com.example.demo.state;
import org.springframework.stereotype.Component;
@Component
public class ApprovedState implements WorkOrderState {
@Override
public void handle(WorkOrderContext ctx, String action) {
throw new IllegalStateException("APPROVED allows no further actions");
}
public WorkOrder.WorkOrderStatus toStatus() { return WorkOrder.WorkOrderStatus.APPROVED; }
}
package com.example.demo.state;
import org.springframework.stereotype.Component;
@Component
public class RejectedState implements WorkOrderState {
@Override
public void handle(WorkOrderContext ctx, String action) {
throw new IllegalStateException("REJECTED allows no further actions");
}
public WorkOrder.WorkOrderStatus toStatus() { return WorkOrder.WorkOrderStatus.REJECTED; }
}

上下文需要根据实体里的 status 拿到对应的状态 Bean。可以用一个 Registry,在启动时把枚举和状态 Bean 对应起来。

package com.example.demo.state;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.EnumMap;
import java.util.Map;
@Configuration
public class WorkOrderStateConfig {
@Bean
public Map<WorkOrder.WorkOrderStatus, WorkOrderState> stateRegistry(
DraftState draft, SubmittedState submitted,
ApprovedState approved, RejectedState rejected) {
Map<WorkOrder.WorkOrderStatus, WorkOrderState> map = new EnumMap<>(WorkOrder.WorkOrderStatus.class);
map.put(WorkOrder.WorkOrderStatus.DRAFT, draft);
map.put(WorkOrder.WorkOrderStatus.SUBMITTED, submitted);
map.put(WorkOrder.WorkOrderStatus.APPROVED, approved);
map.put(WorkOrder.WorkOrderStatus.REJECTED, rejected);
return map;
}
}

WorkOrderContextImpl 里注入这个 Map,在 load(order) 时用 order.getStatus() 从 Map 里取到当前状态 Bean 赋给 statesetState(WorkOrderState state) 时要把状态写回实体,需要状态能提供 toStatus()(上面每个状态类已加),在 setState 里调 state.toStatus() 赋给 order.setStatus(...) 即可。

应用层或服务层加载工单、组装上下文、执行操作,只和 Context 打交道。

package com.example.demo.state;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class WorkOrderService {
private final WorkOrderRepository workOrderRepository;
private final ObjectFactory<WorkOrderContextImpl> contextFactory;
@Transactional
public void submit(Long orderId) {
WorkOrder order = workOrderRepository.findById(orderId).orElseThrow();
WorkOrderContextImpl ctx = contextFactory.getObject();
ctx.load(order);
ctx.perform("submit");
}
@Transactional
public void approve(Long orderId) {
WorkOrder order = workOrderRepository.findById(orderId).orElseThrow();
WorkOrderContextImpl ctx = contextFactory.getObject();
ctx.load(order);
ctx.perform("approve");
}
}

每次操作通过 contextFactory.getObject() 拿一个新的上下文实例,load(order) 后执行动作,状态判断和转换都在状态类里完成。


状态模式把「每个状态下的行为」和「状态转换」从主类里拆出来,放进具体状态类;上下文只持有一个当前状态引用并把请求委托给它,状态类在需要时通过上下文切换状态。这样主类不再堆满 if-else,加状态或改某状态逻辑只需动对应的状态类。上面工单状态机的例子用 Bean 表示各状态,上下文根据实体当前状态从 Registry 取状态并委托,适合审批流、工单、任务等后端场景;若要加「已撤回」等状态,只需新增一个状态类并挂到枚举和 Registry,再在已提交状态里处理「revert」动作并切到新状态即可。