状态模式 (State)
一个对象在不同状态下能干的事不一样:草稿能提交、已提交能审批、已通过就不能再改。如果在主类里用一堆 if (state == DRAFT) ... else if (state == SUBMITTED) ...,每加一个状态或一种操作,这段分支就会再长一截,也很难单测某一种状态的行为。
状态模式的做法是:把「当前是什么状态」和「这个状态下怎么处理请求」交给一个状态对象,主类(上下文)只持有一个当前状态的引用,收到请求时转给这个状态去处理;状态处理完需要切到下一状态时,再通过上下文把当前状态换掉。这样每种状态的行为和转换都收在各自类里,主类保持薄一层,扩展新状态主要是加新类而不是改老分支。
一、问题从哪来?
Section titled “一、问题从哪来?”很多业务里,同一个对象在不同阶段能执行的操作不同:例如工单在草稿时只能保存和提交,提交后只能审批或退回,通过后就只读。最直接的写法是在处理「提交」「审批」的入口处,根据当前状态写一大串 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
四、最小示例:两状态互相切
Section titled “四、最小示例:两状态互相切”不依赖框架,用接口和两个具体状态把「委托 + 切换」跑通。
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 处理,切换到 StateBctx.request(); // StateB 处理下面把「上下文」换成工单、「状态」换成草稿/已提交/已通过/已驳回,用 Bean 注入各状态,工单在处理操作时委托当前状态并完成转换。
五、实战:工单状态机
Section titled “五、实战:工单状态机”后台里常见「工单」「申请单」一类的东西:有固定几个状态(草稿、已提交、已通过、已驳回等),每个状态下允许的操作不同,操作后可能切到下一状态。用状态模式可以把「当前状态下能不能做、做了切到哪」都放进对应的状态类,工单本身只持有一个当前状态引用并转发请求。
这里用四种状态:草稿 (DRAFT)、已提交 (SUBMITTED)、已通过 (APPROVED)、已驳回 (REJECTED)。草稿可提交→已提交;已提交可通过→已通过、驳回→已驳回;已通过/已驳回不再做状态转换,只返回结果。每个状态是一个 Bean,工单(Context)由服务层创建或加载,处理动作时委托给当前状态。
5.1 工单实体与状态枚举
Section titled “5.1 工单实体与状态枚举”工单需要存当前状态,以及业务数据;状态枚举或字符串均可,这里用枚举便于和状态类对应。
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> {}5.2 状态接口与上下文接口
Section titled “5.2 状态接口与上下文接口”状态类需要「处理某种操作」并可能切换状态,这里用「操作类型 + 上下文」作为参数,方便不同操作共用一个入口;你也可以按操作拆成 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 和当前 WorkOrderState,setState 时更新实体里的 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")@RequiredArgsConstructorpublic 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;
@Componentpublic 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;
@Componentpublic 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;
@Componentpublic 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;
@Componentpublic 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;
@Componentpublic 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; }}5.4 状态注册与上下文组装
Section titled “5.4 状态注册与上下文组装”上下文需要根据实体里的 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;
@Configurationpublic 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 赋给 state。setState(WorkOrderState state) 时要把状态写回实体,需要状态能提供 toStatus()(上面每个状态类已加),在 setState 里调 state.toStatus() 赋给 order.setStatus(...) 即可。
5.5 在服务层用
Section titled “5.5 在服务层用”应用层或服务层加载工单、组装上下文、执行操作,只和 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@RequiredArgsConstructorpublic 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」动作并切到新状态即可。