Java 23 种设计模式:从踩坑到精通 | 番外:观察者模式 —— 订单状态通知实战
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 种设计模式:从踩坑到精通》快速导航
- 开篇:系列介绍与目录
- 正篇:Observer 观察者模式 —— 发布-订阅,你每天都在用的模式
- 当前:番外 · 观察者模式 × 订单状态通知(你在这里)
- 创建型模式汇总
- 结构型模式汇总
- 行为型模式汇总
🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》、《电商多平台电子面单对接实战》。技术相通,思路可鉴。
更多推荐





所有评论(0)