Java 23 种设计模式:从踩坑到精通 | 番外:观察者模式 —— 订单状态通知实战

摘要:观察者模式定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。它通过“订阅-通知”机制,将主题与观察者解耦,让通知逻辑与业务逻辑完全分离。本文结合电商订单状态变更通知的场景,完整展示如何让短信、邮件、App 推送等多个服务自动响应订单状态变化,并深入讲解推模型与拉模型的区别、与发布-订阅模式的对比,帮你掌握“状态变化即通知”的设计精髓。

🗺️ 本文阅读地图(3 分钟速览)

  • 为什么订单状态变化要通知多个服务?
  • 观察者核心角色:抽象主题、具体主题、抽象观察者、具体观察者
  • 手写订单通知系统:短信、邮件、App 推送自动响应
  • 拉模型 vs 推模型:哪种更灵活?
  • 面试必问:“观察者模式和发布-订阅模式有什么区别?Spring Event 用了哪个?”

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 正篇:Observer 观察者模式 —— 发布-订阅,你每天都在用的模式 | 当前:番外 · 观察者模式 × 订单状态通知
🔗 返回系列总目录

1. 订单状态变更通知的痛点

在电商系统中,订单状态变化(已支付、已发货、已签收)需要同步通知多个服务:短信通知、邮件通知、App 推送、站内信等。如果直接在订单类中调用各个通知服务:

class Order {
    private SmsService smsService;
    private EmailService emailService;
    private PushService pushService;

    void setState(String state) {
        this.state = state;
        smsService.send(state);       // 直接调用短信
        emailService.send(state);     // 直接调用邮件
        pushService.send(state);      // 直接调用推送
    }
}

这种写法将订单与每个通知服务紧耦合,新增一个通知渠道(如企业微信)就要改订单类。更糟糕的是,用户希望动态订阅/退订某些通知——比如用户退订了短信,就不能再发短信。

观察者模式的解决思路:订单作为“主题”,维护一个观察者列表。状态变化时,订单只管“喊一声”,所有订阅了通知的观察者自动响应。用户可以动态订阅或退订通知,订单代码完全不用修改。

1.1 你的场景该不该用观察者?

判断标准 是 → 用观察者 否 → 用其他方式
一个对象状态变化需通知多个对象
被通知的对象列表可能动态增减
希望通知逻辑与业务逻辑解耦
只有一对一通知,且通知列表固定 直接调用即可

2. 观察者模式 UML(订单状态通知场景)

在这里插入图片描述


3. 完整源码实现

3.1 抽象主题接口 (Subject)

import java.util.List;

/**
 * 抽象主题:定义观察者管理的公共接口
 */
public interface Subject {
    void attach(Observer observer);   // 注册观察者
    void detach(Observer observer);   // 移除观察者
    void notifyObservers();           // 通知所有观察者
}

💬 白话:主题必须提供“订阅”、“退订”、“喊一声”三个能力。这就是观察者模式的核心协议。

3.2 具体主题:订单 (ConcreteSubject)

import java.util.ArrayList;
import java.util.List;

/**
 * 具体主题:订单类,状态变更时通知所有观察者
 */
public class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String state;      // 订单状态
    private String orderId;

    public ConcreteSubject(String orderId) {
        this.orderId = orderId;
    }

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
        System.out.println("📋 [" + observer.getClass().getSimpleName() + "] 已订阅订单通知");
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
        System.out.println("📋 [" + observer.getClass().getSimpleName() + "] 已取消订阅");
    }

    @Override
    public void notifyObservers() {
        System.out.println("\n🔔 订单 [" + orderId + "] 状态变更为【" + state + "】,开始通知所有订阅者:");
        for (Observer observer : observers) {
            observer.update();  // 拉模型:只通知“我变了”,观察者自己取数据
        }
    }

    /** 设置状态并自动触发通知(核心入口) */
    public void setState(String state) {
        this.state = state;
        notifyObservers();  // 状态变更,自动通知
    }

    public String getState() { return state; }
    public String getOrderId() { return orderId; }
}

💬 白话:订单只需要在状态变化时调用 notifyObservers(),它完全不知道有谁在订阅、订阅者会做什么。新增一个通知渠道,只需新写一个观察者类并 attach 进来,订单代码零修改。

3.3 抽象观察者接口 (Observer)

/**
 * 抽象观察者:定义接收通知的更新接口
 */
public interface Observer {
    void update();
}

💬 白话:所有观察者都必须实现 update()——主题喊一声“我变了”,观察者就自己跑去查看最新数据。这就是“拉模型”。

3.4 具体观察者 A:短信通知 (ConcreteObserverA)

/**
 * 具体观察者A:短信通知服务
 */
public class ConcreteObserverA implements Observer {
    private String phone;
    private ConcreteSubject subject;

    public ConcreteObserverA(ConcreteSubject subject, String phone) {
        this.subject = subject;
        this.phone = phone;
    }

    @Override
    public void update() {
        System.out.println("   📱 [短信通知] 发送至 " + phone + ":");
        System.out.println("      您的订单 " + subject.getOrderId() + " 状态更新为:" + subject.getState());
    }
}

3.5 具体观察者 B:邮件通知 (ConcreteObserverB)

/**
 * 具体观察者B:邮件通知服务
 */
public class ConcreteObserverB implements Observer {
    private String email;
    private ConcreteSubject subject;

    public ConcreteObserverB(ConcreteSubject subject, String email) {
        this.subject = subject;
        this.email = email;
    }

    @Override
    public void update() {
        System.out.println("   📧 [邮件通知] 发送至 " + email + ":");
        System.out.println("      【订单通知】您的订单 " + subject.getOrderId() + " 状态更新为:" + subject.getState());
    }
}

3.6 具体观察者 C:App 推送 (ConcreteObserverC)

/**
 * 具体观察者C:App 推送服务
 */
public class ConcreteObserverC implements Observer {
    private String userId;
    private ConcreteSubject subject;

    public ConcreteObserverC(ConcreteSubject subject, String userId) {
        this.subject = subject;
        this.userId = userId;
    }

    @Override
    public void update() {
        System.out.println("   🔔 [App推送] 推送给用户 " + userId + ":");
        System.out.println("      订单 " + subject.getOrderId() + " 状态更新:" + subject.getState());
    }
}

💬 白话:三个观察者实现完全相同的 update() 接口,但内部逻辑各异——发短信、发邮件、推 App。它们都持有 subject 引用,在 update() 中主动拉取所需数据。

3.7 客户端测试

public class Client {
    public static void main(String[] args) {
        // 1. 创建主题(订单)
        ConcreteSubject order = new ConcreteSubject("20240723001");

        // 2. 创建观察者并注册
        Observer sms = new ConcreteObserverA(order, "138****1234");
        Observer email = new ConcreteObserverB(order, "user@example.com");
        Observer push = new ConcreteObserverC(order, "U10086");

        order.attach(sms);
        order.attach(email);
        order.attach(push);

        // 3. 模拟订单状态变更(自动触发通知)
        order.setState("已支付");
        order.setState("已发货");

        // 4. 取消某个观察者
        System.out.println("\n--- 用户退订短信通知 ---");
        order.detach(sms);

        // 5. 再次状态变更
        order.setState("已签收");
    }
}

4. 运行结果

📋 [ConcreteObserverA] 已订阅订单通知
📋 [ConcreteObserverB] 已订阅订单通知
📋 [ConcreteObserverC] 已订阅订单通知

🔔 订单 [20240723001] 状态变更为【已支付】,开始通知所有订阅者:
   📱 [短信通知] 发送至 138****1234:
      您的订单 20240723001 状态更新为:已支付
   📧 [邮件通知] 发送至 user@example.com:
      【订单通知】您的订单 20240723001 状态更新为:已支付
   🔔 [App推送] 推送给用户 U10086:
      订单 20240723001 状态更新:已支付

🔔 订单 [20240723001] 状态变更为【已发货】,开始通知所有订阅者:
   📱 [短信通知] 发送至 138****1234:
      您的订单 20240723001 状态更新为:已发货
   📧 [邮件通知] 发送至 user@example.com:
      【订单通知】您的订单 20240723001 状态更新为:已发货
   🔔 [App推送] 推送给用户 U10086:
      订单 20240723001 状态更新:已发货

--- 用户退订短信通知 ---
📋 [ConcreteObserverA] 已取消订阅

🔔 订单 [20240723001] 状态变更为【已签收】,开始通知所有订阅者:
   📧 [邮件通知] 发送至 user@example.com:
      【订单通知】您的订单 20240723001 状态更新为:已签收
   🔔 [App推送] 推送给用户 U10086:
      订单 20240723001 状态更新:已签收

5. 核心角色回顾

角色 职责 对应代码
Subject 维护观察者列表,提供注册/移除/通知 Subject
ConcreteSubject 存储状态,状态变更时通知 ConcreteSubject
Observer 定义接收通知的接口 Observer
ConcreteObserver 响应状态变化,执行各自逻辑 ConcreteObserverA/B/C

6. 拉模型 vs 推模型

对比项 拉模型(本例) 推模型
通知方式 主题只通知“我变了” 主题把所有数据推送过来
灵活性 高,观察者按需取数据 低,观察者被动接收
耦合度 观察者需持有主题引用 主题需知道观察者需要什么数据
适用场景 数据量大、观察者需求各异 数据量小、所有观察者需求一致

💡 白话:本例采用拉模型——update() 不带参数,观察者自己调用 subject.getState() 获取所需数据。新增一个只关心订单号的观察者,不需要主题改动任何代码。

7. 观察者模式 vs 发布-订阅模式 vs 中介者模式

对比项 观察者模式 发布-订阅模式 中介者模式
耦合度 主题知道观察者列表(松耦合) 发布者与订阅者完全解耦,通过 Broker 中转 同事通过中介者协调
通信方式 直接调用 update() 通过消息队列/事件总线 通过 notify() 中转
适用层级 进程内(单应用) 跨进程、分布式 进程内,多对象交互
典型应用 Spring Event、Swing 监听器 RabbitMQ、Kafka、Redis Pub/Sub 聊天室、DispatcherServlet

💡 一句话记忆:观察者是“直接订阅,主题知情”;发布-订阅是“隔了一层 Broker,发布者不知道谁订阅”;中介者是“中间人协调双向交互”。

8. 观察者模式的优缺点

优点 缺点
主题与观察者松耦合,各自独立变化 通知顺序不可控,依赖集合实现
一对多广播自动完成 观察者多时,性能有开销
新增观察者无需修改主题代码 可能导致循环依赖(观察者修改主题)
运行时动态订阅/退订 通知链长时难以调试

9. 六大设计原则体现

原则 体现
单一职责 主题管状态,观察者管响应
开闭原则 新增通知渠道无需修改订单代码
里氏替换 所有观察者可替换 Observer 接口
依赖倒置 主题依赖抽象 Observer 接口
接口隔离 Observer 只有 update() 一个方法
迪米特法则 主题只知道 Observer 接口,不知具体实现

附 观察者模式 UML源码(订单状态通知场景)

@startuml
title Java 23 种设计模式:从踩坑到精通
footer 折哥 | 智能物流与Java实战

' 1. 全局样式配置
skinparam backgroundColor #FEFEFE
skinparam shadowing false
skinparam classBorderColor #333333
skinparam classFontColor #1A1A1A
skinparam classFontSize 14
skinparam noteFontSize 12
skinparam noteFontColor #555555
skinparam arrowColor #555555
skinparam classBackgroundColor #F9F9F9
skinparam interface {
    BackgroundColor #E8F5E9
    BorderColor #2E7D32
}

' 2. 抽象主题
interface Subject {
    + attach(Observer)
    + detach(Observer)
    + notifyObservers()
}
note right of Subject
  <b>抽象主题</b>
  --
  维护观察者集合
  提供注册、移除、通知的统一接口
end note

' 3. 具体主题
class ConcreteSubject implements Subject {
    - state : String
    + getState() : String
    + setState(String)
}
note right of ConcreteSubject
  <b>具体主题</b>
  --
  存储状态数据
  状态变更时主动通知所有观察者
end note

' 4. 抽象观察者
interface Observer {
    + update()
}
note right of Observer
  <b>抽象观察者</b>
  --
  定义接收通知的更新接口
  所有观察者必须实现此方法
end note

' 5. 具体观察者A
class ConcreteObserverA implements Observer {
    + update()
}
note right of ConcreteObserverA
  <b>具体观察者A</b>
  --
  对主题状态变化做出响应
  例如:短信通知服务
end note

' 6. 具体观察者B
class ConcreteObserverB implements Observer {
    + update()
}
note right of ConcreteObserverB
  <b>具体观察者B</b>
  --
  对主题状态变化做出响应
  例如:邮件通知服务
end note

' 7. 关系连线
Subject o-down-> Observer : 通知
ConcreteSubject .up.> Subject : 实现
ConcreteObserverA .up.> Observer : 实现
ConcreteObserverB .up.> Observer : 实现

@enduml

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》《电商多平台电子面单对接实战》。技术相通,思路可鉴。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐