一、为什么我们需要状态模式

在日常开发中,我们经常会遇到需要根据对象的不同状态来执行不同行为的场景。比如一个订单可能有"待支付"、"已支付"、"已发货"、"已完成"等状态,每个状态下订单的行为都不相同。最直观的做法就是使用大量的if-else或者switch-case语句来判断当前状态并执行相应逻辑。

这种做法的缺点很明显:随着状态数量的增加,代码会变得越来越臃肿,维护成本急剧上升。每次新增或修改状态都需要修改原有的判断逻辑,违反了开闭原则。而且各种状态的行为逻辑混杂在一起,可读性也很差。

这时候,状态模式就能派上用场了。它允许对象在其内部状态改变时改变它的行为,看起来就像是对象改变了它的类一样。通过将每个状态的行为封装到独立的类中,我们可以避免庞大的条件判断语句,使代码更加清晰和易于扩展。

二、状态模式的基本结构

状态模式主要包含三个角色:

  1. Context(上下文):定义客户端感兴趣的接口,维护一个具体状态类的实例
  2. State(抽象状态):定义一个接口,用于封装与Context的特定状态相关的行为
  3. ConcreteState(具体状态):实现State接口,每个子类实现一个与Context状态相关的行为

让我们用Java代码来展示这个结构:

// 抽象状态类
interface OrderState {
    void handle(OrderContext context);
}

// 具体状态类:待支付状态
class UnpaidState implements OrderState {
    @Override
    public void handle(OrderContext context) {
        System.out.println("订单待支付,可以执行支付操作");
        // 支付成功后切换到已支付状态
        context.setState(new PaidState());
    }
}

// 具体状态类:已支付状态
class PaidState implements OrderState {
    @Override
    public void handle(OrderContext context) {
        System.out.println("订单已支付,可以执行发货操作");
        // 发货后切换到已发货状态
        context.setState(new ShippedState());
    }
}

// 上下文类
class OrderContext {
    private OrderState state;
    
    public OrderContext(OrderState state) {
        this.state = state;
    }
    
    public void setState(OrderState state) {
        this.state = state;
    }
    
    public void request() {
        state.handle(this);
    }
}

// 客户端代码
public class StatePatternDemo {
    public static void main(String[] args) {
        OrderContext context = new OrderContext(new UnpaidState());
        
        // 模拟订单状态流转
        context.request();  // 待支付状态处理
        context.request();  // 已支付状态处理
        context.request();  // 已发货状态处理
    }
}

三、状态模式的进阶应用

在实际项目中,状态模式的应用往往更加复杂。让我们看一个更贴近现实的电商订单状态管理示例:

// 订单状态接口
interface OrderStatus {
    void pay(Order order);
    void ship(Order order);
    void receive(Order order);
    void cancel(Order order);
}

// 待支付状态实现
class UnpaidStatus implements OrderStatus {
    @Override
    public void pay(Order order) {
        System.out.println("订单支付成功");
        order.setStatus(new PaidStatus());
    }

    @Override
    public void ship(Order order) {
        System.out.println("订单尚未支付,不能发货");
    }

    @Override
    public void receive(Order order) {
        System.out.println("订单尚未支付,不能收货");
    }

    @Override
    public void cancel(Order order) {
        System.out.println("订单已取消");
        order.setStatus(new CancelledStatus());
    }
}

// 已支付状态实现
class PaidStatus implements OrderStatus {
    @Override
    public void pay(Order order) {
        System.out.println("订单已支付,无需重复支付");
    }

    @Override
    public void ship(Order order) {
        System.out.println("订单已发货");
        order.setStatus(new ShippedStatus());
    }

    @Override
    public void receive(Order order) {
        System.out.println("订单尚未发货,不能收货");
    }

    @Override
    public void cancel(Order order) {
        System.out.println("订单已取消,退款处理中");
        order.setStatus(new CancelledStatus());
    }
}

// 订单类
class Order {
    private OrderStatus status;
    
    public Order() {
        this.status = new UnpaidStatus();
    }
    
    public void setStatus(OrderStatus status) {
        this.status = status;
    }
    
    public void pay() {
        status.pay(this);
    }
    
    public void ship() {
        status.ship(this);
    }
    
    public void receive() {
        status.receive(this);
    }
    
    public void cancel() {
        status.cancel(this);
    }
}

// 使用示例
public class ECommerceDemo {
    public static void main(String[] args) {
        Order order = new Order();
        
        order.pay();    // 支付
        order.ship();   // 发货
        order.receive();// 收货
        order.cancel(); // 尝试取消(此时会提示不能取消)
    }
}

这个示例展示了更完整的订单状态流转过程,每个状态都明确定义了哪些操作是允许的,哪些是不允许的。通过状态模式,我们将各种状态的行为逻辑清晰地分离到了不同的类中。

四、状态模式与策略模式的比较

状态模式和策略模式在结构上非常相似,但它们的目的不同:

  1. 策略模式:客户端主动选择不同的算法策略,策略之间通常是独立的
  2. 状态模式:状态转换由上下文或状态类自身控制,客户端不直接指定状态

让我们通过代码来比较这两种模式:

// 策略模式示例
interface PaymentStrategy {
    void pay(double amount);
}

class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用信用卡支付:" + amount);
    }
}

class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("使用PayPal支付:" + amount);
    }
}

class PaymentContext {
    private PaymentStrategy strategy;
    
    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void executePayment(double amount) {
        strategy.pay(amount);
    }
}

// 状态模式示例
interface TrafficLightState {
    void change(TrafficLight light);
}

class RedLightState implements TrafficLightState {
    @Override
    public void change(TrafficLight light) {
        System.out.println("红灯亮,等待");
        light.setState(new GreenLightState());
    }
}

class GreenLightState implements TrafficLightState {
    @Override
    public void change(TrafficLight light) {
        System.out.println("绿灯亮,通行");
        light.setState(new YellowLightState());
    }
}

class TrafficLight {
    private TrafficLightState state;
    
    public TrafficLight() {
        this.state = new RedLightState();
    }
    
    public void setState(TrafficLightState state) {
        this.state = state;
    }
    
    public void change() {
        state.change(this);
    }
}

从示例中可以看出,策略模式的选择权在客户端,而状态模式的转换通常由内部逻辑控制。

五、状态模式的最佳实践

在实际项目中使用状态模式时,有几点需要注意:

  1. 状态对象的创建:可以使用单例模式来共享状态对象,因为状态对象通常是无状态的
  2. 状态转换的触发:可以在Context中定义各种触发状态转换的方法
  3. 状态转换的约束:应该在状态类中定义哪些转换是合法的
  4. 与观察者模式的结合:当状态发生变化时,可以通知相关观察者

下面是一个结合了这些实践的例子:

// 状态接口
interface DocumentState {
    void publish(Document document);
    void archive(Document document);
}

// 草稿状态
class DraftState implements DocumentState {
    private static final DraftState INSTANCE = new DraftState();
    
    private DraftState() {}
    
    public static DraftState getInstance() {
        return INSTANCE;
    }
    
    @Override
    public void publish(Document document) {
        System.out.println("发布文档");
        document.setState(PublishedState.getInstance());
    }
    
    @Override
    public void archive(Document document) {
        System.out.println("草稿不能直接归档");
    }
}

// 文档类
class Document {
    private DocumentState state;
    private List<DocumentObserver> observers = new ArrayList<>();
    
    public Document() {
        this.state = DraftState.getInstance();
    }
    
    public void setState(DocumentState state) {
        this.state = state;
        notifyObservers();
    }
    
    public void publish() {
        state.publish(this);
    }
    
    public void archive() {
        state.archive(this);
    }
    
    public void addObserver(DocumentObserver observer) {
        observers.add(observer);
    }
    
    private void notifyObservers() {
        for (DocumentObserver observer : observers) {
            observer.update(this);
        }
    }
}

// 观察者接口
interface DocumentObserver {
    void update(Document document);
}

六、状态模式的优缺点分析

优点:

  1. 将状态相关的行为局部化,便于理解和维护
  2. 避免了大量的条件判断语句
  3. 符合单一职责原则,每个状态都是一个独立的类
  4. 符合开闭原则,新增状态不需要修改现有代码

缺点:

  1. 增加了系统中类的数量
  2. 状态转换逻辑可能分散在各个状态类中
  3. 对于简单的状态机,可能会显得过于复杂

七、状态模式的应用场景

状态模式特别适用于以下场景:

  1. 对象的行为取决于它的状态,并且必须在运行时根据状态改变行为
  2. 代码中包含大量与对象状态相关的条件语句
  3. 状态转换逻辑复杂,或者状态数量较多
  4. 需要清晰地区分不同状态下的行为

常见的使用案例包括:

  • 订单状态管理
  • 工作流引擎
  • 游戏角色状态
  • UI控件状态(如按钮的禁用/启用状态)
  • 网络连接状态管理

八、总结

状态模式是一种强大的设计模式,它通过将对象的状态封装成独立的类,避免了复杂的条件判断,使代码更加清晰和易于扩展。虽然它会增加一些类的数量,但对于复杂的状态管理场景来说,这种代价是值得的。

在Java中实现状态模式时,我们可以充分利用接口和类的特性,创建清晰的状态层次结构。通过将状态转换逻辑封装在状态类中,我们可以确保状态转换的正确性,并使上下文类保持简洁。

当你的代码中出现大量与状态相关的if-else语句时,不妨考虑使用状态模式来重构。它不仅能改善代码结构,还能为未来的扩展打下良好的基础。