状态模式:让对象的行为随状态而变
状态模式,说白了就是让一个对象在内部状态改变时,它的行为也跟着变。听起来像不像一个对象在玩变脸?嗯,差不多就是这个意思。
我在做电商系统的时候,订单状态流转这块儿,一开始用了一堆if-else,后来维护起来简直想哭。直到我用了状态模式,整个世界清净了。
状态模式的核心三要素
状态模式有三个主角:状态接口、具体状态、上下文。我一个个说。
1. 状态接口(State)
定义所有具体状态必须实现的方法。说白了就是一个契约,所有状态都得遵守。
// Java 版本
public interface OrderState {
void pay(Long orderId);
void cancel(Long orderId);
void ship(Long orderId);
void confirm(Long orderId);
}
// C++ 版本
class OrderState {
public:
virtual ~OrderState() = default;
virtual void pay(long orderId) = 0;
virtual void cancel(long orderId) = 0;
virtual void ship(long orderId) = 0;
virtual void confirm(long orderId) = 0;
};
2. 具体状态(ConcreteState)
每个具体状态实现状态接口,并且只关心自己状态下的行为逻辑。比如「待支付」状态只处理支付和取消,其他操作直接抛异常。
// Java 版本
public class UnpaidState implements OrderState {
@Override
public void pay(Long orderId) {
System.out.println("订单 " + orderId + " 支付成功,状态变为待发货");
// 这里会切换上下文的状态
}
@Override
public void cancel(Long orderId) {
System.out.println("订单 " + orderId + " 已取消");
}
@Override
public void ship(Long orderId) {
throw new UnsupportedOperationException("未支付不能发货");
}
@Override
public void confirm(Long orderId) {
throw new UnsupportedOperationException("未支付不能确认收货");
}
}
// C++ 版本
class UnpaidState : public OrderState {
public:
void pay(long orderId) override {
std::cout << "订单 " << orderId << " 支付成功,状态变为待发货" << std::endl;
}
void cancel(long orderId) override {
std::cout << "订单 " << orderId << " 已取消" << std::endl;
}
void ship(long orderId) override {
throw std::runtime_error("未支付不能发货");
}
void confirm(long orderId) override {
throw std::runtime_error("未支付不能确认收货");
}
};
3. 上下文(Context)
上下文持有当前状态的引用,并把所有请求委托给当前状态对象。它负责状态之间的切换。
// Java 版本
public class OrderContext {
private OrderState currentState;
public OrderContext() {
// 初始状态为待支付
this.currentState = new UnpaidState();
}
public void setState(OrderState state) {
this.currentState = state;
}
public void pay(Long orderId) {
currentState.pay(orderId);
// 支付成功后切换到待发货状态
if (currentState instanceof UnpaidState) {
setState(new PaidState());
}
}
public void cancel(Long orderId) {
currentState.cancel(orderId);
}
public void ship(Long orderId) {
currentState.ship(orderId);
if (currentState instanceof PaidState) {
setState(new ShippedState());
}
}
public void confirm(Long orderId) {
currentState.confirm(orderId);
if (currentState instanceof ShippedState) {
setState(new CompletedState());
}
}
}
// C++ 版本
class OrderContext {
private:
std::unique_ptr<OrderState> currentState;
public:
OrderContext() : currentState(std::make_unique<UnpaidState>()) {}
void setState(std::unique_ptr<OrderState> state) {
currentState = std::move(state);
}
void pay(long orderId) {
currentState->pay(orderId);
if (dynamic_cast<UnpaidState*>(currentState.get())) {
setState(std::make_unique<PaidState>());
}
}
void cancel(long orderId) {
currentState->cancel(orderId);
}
void ship(long orderId) {
currentState->ship(orderId);
if (dynamic_cast<PaidState*>(currentState.get())) {
setState(std::make_unique<ShippedState>());
}
}
void confirm(long orderId) {
currentState->confirm(orderId);
if (dynamic_cast<ShippedState*>(currentState.get())) {
setState(std::make_unique<CompletedState>());
}
}
};
订单状态流转实战
我记得有一次做电商项目,订单状态有6种:待支付、已支付、已发货、已确认、已完成、已取消。每个状态之间的流转规则都不一样。
你想想看,如果用if-else写,每个方法里都得判断当前状态,然后决定能不能执行。代码会变成一坨浆糊。
用状态模式之后,每个状态类只关心自己的逻辑。新增一个状态,比如「退款中」,只需要加一个新类,不用改任何现有代码。这就是开闭原则的体现。
核心思想:状态模式把每个状态的行为封装成一个独立的类,状态切换就是换一个对象。这样代码清晰,扩展方便。
电梯状态切换
电梯的状态切换也是个经典例子。电梯有开门、关门、运行、停止、故障等状态。每个状态下能做的操作不一样。
// Java 版本 - 电梯状态接口
public interface ElevatorState {
void openDoor();
void closeDoor();
void moveUp();
void moveDown();
void stop();
void handleEmergency();
}
// C++ 版本
class ElevatorState {
public:
virtual ~ElevatorState() = default;
virtual void openDoor() = 0;
virtual void closeDoor() = 0;
virtual void moveUp() = 0;
virtual void moveDown() = 0;
virtual void stop() = 0;
virtual void handleEmergency() = 0;
};
比如「开门状态」下,你不能移动电梯,只能关门。而「运行状态」下,你不能开门,只能停止或继续移动。
个人经验:我在做电梯控制系统时,一开始把状态切换逻辑写在了上下文里,结果上下文越来越臃肿。后来我把状态切换逻辑放到了具体状态类中,每个状态类在完成操作后,告诉上下文应该切换到哪个状态。这样上下文就变得很薄,只负责委托和状态持有。
状态模式 vs 策略模式
很多人容易把状态模式和策略模式搞混。我简单说下区别:
| 对比维度 | 状态模式 | 策略模式 |
|---|---|---|
| 核心目的 | 对象行为随状态变化 | 算法可替换 |
| 状态切换 | 状态之间可以自动切换 | 策略由客户端指定,一般不切换 |
| 状态数量 | 有限且已知 | 可无限扩展 |
| 典型场景 | 订单、电梯、工作流 | 排序算法、支付方式 |
说白了,状态模式关注的是「状态流转」,策略模式关注的是「算法选择」。一个对象的状态会变,但策略一般不会在运行时自动切换。
状态模式的优缺点
- 优点:
- 消除了庞大的条件分支语句
- 每个状态独立成类,符合单一职责
- 新增状态不影响现有代码,符合开闭原则
- 状态流转逻辑清晰,容易维护
- 缺点:
- 状态多了会导致类数量膨胀
- 状态切换逻辑分散在各个状态类中,可能不好追踪
- 如果状态很少且不常变,用状态模式反而过度设计
避坑指南:我曾经在一个项目中,把状态模式用在了只有3个状态的场景。结果代码反而比if-else更复杂。后来我反思,状态模式适合状态多、流转规则复杂的场景。如果状态少于4个,且流转简单,用if-else或者枚举反而更直接。
状态模式的核心流程图
什么时候用状态模式
我个人总结了几条经验:
- 对象的行为依赖于它的状态,并且状态会变化
- 状态数量不少于4个,且流转规则复杂
- 每个状态下的行为差异很大
- 你希望新增状态时不影响现有代码
如果满足以上条件,状态模式就是你的好选择。如果不满足,用枚举+简单判断可能更合适。
小技巧:状态模式可以和工厂模式结合使用。比如在上下文中,用一个状态工厂来创建状态对象,这样状态切换的逻辑就更清晰了。我在一个工作流引擎中就是这么做的,效果很好。
好了,状态模式就讲到这里。记住一句话:状态模式让对象的行为随状态而变,而不是用if-else硬编码。下次你遇到状态流转的场景,不妨试试这个模式。