常见设计模式-观察者模式

Java 设计模式 · 观察者模式(Observer Pattern)系统学习文档

一、总览(先看这张图,后面全部围绕它展开)

复制代码
                    【谁的变化,要通知谁】

   ┌─────────────────────────────────────────────────────────┐
   │                    Subject 被观察者                      │
   │          (持有状态 + 持有一组 Observer 的引用)            │
   │                                                         │
   │   register(observer)      ← 注册:以后有事叫我            │
   │   unregister(observer)    ← 反注册:别再叫我了            │
   │   notifyAll()             ← 状态变了,挨个通知            │
   └───────────────┬─────────────────────────────────────────┘
                   │  状态变化时主动遍历通知
       ┌───────────┼───────────┬───────────────┐
       ▼           ▼           ▼               ▼
   ┌────────┐  ┌────────┐  ┌────────┐     ┌────────┐
   │ 观察者1 │  │ 观察者2 │  │ 观察者3 │ ... │ 观察者N │
   │ 短信    │  │ 邮件    │  │ 站内信  │     │ 数据看板│
   └────────┘  └────────┘  └────────┘     └────────┘
        ▲           ▲           ▲
        └───────────┴───────────┘
         全部依赖同一个 Observer 接口,
         彼此不认识,新增/删除谁都不用改 Subject 的代码

一句话抓住本质:

一对多依赖 :当 A 的状态改变时,所有依赖 A 的对象自动收到通知并更新自己。

关键在于 ------ A 不认识具体的 B/C/D ,只认识一个 Observer 接口。


二、概念介绍

2.1 定义

来源 说法
GoF《设计模式》 定义对象间的一种一对多的依赖关系,当一个对象(Subject)的状态发生改变时,所有依赖于它的对象(Observer)都得到通知并自动更新。
更口语的说法 "订阅---通知":我订阅你,你有动静就喊我一声。

2.2 四个核心角色

角色 职责 本文对应
Subject(主题/被观察者) 持有观察者列表,状态变化时遍历通知 SimpleSubject / SafeSubject
Observer(观察者) 定义收到通知后的更新动作 interface Observer
ConcreteSubject 具体的主题实现(有真实业务状态) 订单状态变更的 Subject
ConcreteObserver 具体的观察者实现 SmsObserver、EmailObserver

注意 :骨架里 SimpleSubject 同时充当了 Subject 和 ConcreteSubject,这是教学时的简化。真实项目中如果只有一种主题,不必强行拆两层。

2.3 它属于哪一类模式

  • 目的:对象行为型模式(关注对象之间怎么协作)
  • 关系:解耦"事件产生方"和"事件响应方"
  • 核心手法 :面向接口编程 + 组合 (用一个 List<Observer> 替代硬编码调用)

三、为什么需要它(问题 → 方案 → 为什么 → 收益)

3.1 典型问题:硬编码的"扇出"调用

假设订单状态一变,要通知短信、邮件、站内信三个渠道:

❌ 反面写法(最常见)

java 复制代码
public void changeStatus(Order order) {
    this.status = order.status;
    smsService.send(order);        // 硬编码依赖 1
    emailService.send(order);      // 硬编码依赖 2
    siteService.send(order);       // 硬编码依赖 3
}

问题出在哪:

症状 后果
加一个渠道(如"App 推送") 必须改订单类 ,违反开闭原则(对扩展开放、对修改关闭)
渠道逻辑变了 又要去改订单类,职责混杂
渠道之间有先后依赖 通知逻辑越堆越乱,订单类越来越胖
想临时关掉邮件通知 只能改代码,没法运行期控制

根本矛盾 :订单类本该只管订单,却被逼着认识一堆通知渠道。

3.2 方案:引入 Observer 接口

✅ 观察者写法

java 复制代码
public void changeStatus(Order order) {
    this.status = order.status;
    for (Observer o : observers) {   // 只认识接口,不认识具体是谁
        o.update(order);
    }
}

3.3 为什么这样就对了

维度 解释
依赖方向反转 订单类不再依赖 3 个具体服务,只依赖 1 个抽象 Observer ------ 依赖倒置
满足开闭原则 加渠道 = 写一个新 Observer 实现 + attach(),订单类一行不改
职责单一 订单只负责"喊一声",怎么处理是各观察者自己的事
运行期可控 attach/detach 让通知对象集可以动态变化,不必重启

3.4 收益总结

复制代码
改动成本:
  硬编码:  新增渠道 = 改订单类 + 回归测试订单逻辑   (耦合度 高)
  观察者:  新增渠道 = 新建一个类 + 一行 attach      (耦合度 低)

风险面:
  硬编码:  通知逻辑出问题可能波及订单核心流程
  观察者:  靠异常隔离,单个观察者出错不会拖垮订单(见 5.2)

面试表达 :观察者模式的本质是把"一对多的扇出调用"变成"面向接口的广播",用运行期的动态订阅替代编译期的硬编码依赖。


四、观察者的两种数据传递方式:推模式 vs 拉模式

这是观察者模式最重要的一个分支,面试高频。

4.1 推模式(Push)------ 我把数据塞给你

java 复制代码
void update(Order order);   // 被观察者直接把完整数据传过去
  • 优点:观察者拿到即用,简单直接
  • 缺点 :Subject 必须知道 Observer 需要什么;数据结构一变,所有 Observer 都受影响;观察者可能只关心一个字段却被迫接收全量

4.2 拉模式(Pull)------ 我只告诉你"变了",你自己来取

java 复制代码
void update(Subject source);        // 只给个信号/引用
// 观察者内部:
Order o = source.getLatest();       // 自己回查
  • 优点 :Subject 只需提供通用取数接口,耦合更松;观察者按需取字段
  • 缺点 :观察者要多一次取数,且需要能访问 Subject 的内部状态(通常暴露 getter)

4.3 怎么选

场景 建议
数据小而稳定、观察者都需要全量 推模式(省事)
数据大/字段多、观察者各取所需 拉模式(省流量、松耦合)
拿不准 推"精简版",或推一个"事件对象"让观察者自己判断要不要继续取

实测代码 Demo1 走推 ,Demo3 走拉,附录 A 可对照看输出差异。


五、代码讲解(从骨架到工程化)

以下代码全部实测可运行,完整源码见附录 B。

5.1 最小骨架(能跑通的最简形态)

java 复制代码
// 观察者接口
interface Observer {
    void update(Order order);
}

// 被观察者
class SimpleSubject {
    private final List<Observer> observers = new CopyOnWriteArrayList<>();
    private Order latest;

    public void attach(Observer o)   { observers.add(o); }
    public void detach(Observer o)   { observers.remove(o); }

    public void changeStatus(Order o) {
        this.latest = o;
        notifyAllObservers();
    }

    private void notifyAllObservers() {
        for (Observer o : observers) {
            o.update(latest);              // 推模式:直接给数据
        }
    }
}

讲解要点:

  1. observers 用 接口类型 装 ------ 这是解耦的关键,具体是谁无所谓
  2. attach/detach 让集合运行期可变,这是相对硬编码的最大优势
  3. latest 保存最新状态,notifyAllObservers() 遍历广播

实测验证了三件事(附录 A Demo1):

  • ✅ 新增观察者后,下一次通知能收到
  • ✅ 反注册后,不再收到
  • ✅ 已注册的观察者每次都收到

5.2 工程化要点一:异常隔离(⭐ 高含金量)

问题 :如果通知循环里某个观察者抛异常,for 循环会直接中断,后面的观察者全部收不到通知。

java 复制代码
for (Observer o : observers) {
    o.update(latest);   // ← 这里抛异常,循环终止,3号4号再也收不到
}

方案 :给每个观察者单独 try-catch。

为什么 :通知是广播,性质上应是"尽力送达"------一个渠道挂了(比如邮件服务连接池耗尽),不该让订单状态通知整体失败。

收益 :故障隔离,单点故障不扩散。这也是"观察者模式在生产环境必须做的加固"。

java 复制代码
for (Observer o2 : List.copyOf(observers)) {
    try {
        o2.update(latest);
    } catch (Exception e) {
        // 吞掉并记录,不让单个观察者拖垮整条通知链
        System.out.println("  [隔离] 观察者 " + o2.name() + " 异常被吞:" + e.getMessage());
    }
}

实测结果 (附录 A Demo2)------坏观察者抛异常,后面的人照样收到:

复制代码
  [短信]   138****0001 收到:Order{B2002, 退款中}
  [隔离] 观察者 坏观察者 异常被吞:连接池耗尽
  [邮件]   li@x.com 收到:Order{B2002, 退款中}     ← 没被影响

5.3 工程化要点二:线程安全与遍历(⭐)

问题 :如果一个线程在 changeStatus() 遍历通知,另一个线程同时 attach/detach(),会抛 ConcurrentModificationException。

两个方案:

方案 做法 适用
A. 换容器 new CopyOnWriteArrayList<>() 写极少、读极多(通知列表正是此场景)
B. 遍历前拷贝 for (Observer o : List.copyOf(observers)) 通用兜底,不依赖容器实现

为什么推荐 COW:观察者列表几乎总是"注册/反注册很少、每秒通知很多",COW 写时复制的代价正好落在低频操作上,读时无锁、迭代零冲突。

实测结果(附录 A Demo4)------并发通知无异常:

复制代码
  并发下无 ConcurrentModificationException

诚实标注 :Demo4 是单次并发 的验证,能证明遍历不会抛 CME;不等于证明了高并发下的完整正确性(如通知顺序、可见性等)。若要做严格验证,需要压力测试------本测试未做。


5.4 ⭐ 实测翻车点:反注册失效(最容易踩的坑)

这是我写文档时真实跑出来的 bug,值得单独讲。

现象 :写了 detach(),以为反注册成功,结果观察者还在收通知。

java 复制代码
subject.attach(new EmailObserver("wang@x.com"));
subject.detach(new EmailObserver("wang@x.com"));   // 同样的内容,但是【新对象】
subject.changeStatus(order);
// 预期:邮件不收。实际:邮件照样收到!

根因 :List.remove(Object) 内部用 equals 比较。EmailObserver 没重写 equals,走 Object 的引用相等 ------new 出来的两个对象引用不同 → 移除失败,但也不报错,静默失效。

为什么危险 :这是静默 bug------不抛异常、不报错,只是通知停不下来,在生产环境表现为"用户已经取消订阅了,还在收到短信"。

三个解法(实测对比见附录 A Demo0):

解法 写法 评价
A. 持有同一引用 EmailObserver ref = ...; attach(ref); detach(ref); 最稳,推荐
B. 重写 equals/hashCode 让"同内容"视为同一观察者 适合"按身份订阅"的场景
C. 用唯一 key 管理 Map<String, Observer> 按 id 移除 工程上最可控

实测输出(三种写法同框对比):

复制代码
[坑A] 用【新实例】反注册 + 未重写 equals → 移除失败
  [邮件]   wang@x.com 收到:Order{A000, 测试}      ← 仍收到,detach 没生效
[坑B] 用【同一个引用】反注册 → 成功
  ↑ 邮件没收到 = detach 生效
[坑C] 重写 equals/hashCode 后,新实例也能移除
  ↑ 邮件没收到 = equals 让 detach 生效

面试价值 :能主动讲出这个坑,说明你真写过、真踩过,而不是背概念------这是"深水区"信号(见第八节分级)。


5.5 为什么不用 JDK 自带的 java.util.Observable

实测事实(JDK 21,编译即警告):

复制代码
Observable 类上注解: @java.lang.Deprecated(forRemoval=false, since="9")
Observer 接口上注解: @java.lang.Deprecated(forRemoval=false, since="9")

它为什么被废弃:

缺陷 说明
继承而非接口 extends Observable 占用唯一父类,无法复用
"半成品"封装 setChanged() 要手动调用才触发通知,状态管理割裂
线程不安全 内部用 Vector + 手工 notify,并发下易出问题
设计陈旧 未考虑泛型、Lambda 等现代特性

结论 :JDK 9 起废弃,新代码不要用。正确做法是:

  • 简单场景:自己写 10 行(如本文骨架)
  • Java 8+:用 Consumer/回调接口或事件总线
  • Spring:用 ApplicationEventPublisher(见 6.3)

六、常见使用场景

6.1 JDK 标准库中的观察者

位置 形式
java.beans.PropertyChangeListener JavaBean 属性变化监听(未废弃,可用)
Swing/AWT 事件模型 按钮点击等 UI 事件(ActionListener)
java.util.Observer/Observable 已废弃(JDK 9),见 5.5
InputStream 的 available/自定义流包装 部分流处理采用回调式设计(弱相关)

诚实标注 :前两项是确定的标准用法;第三项已废弃。第四项仅为"回调式设计"的类比,不算严格观察者模式,面试时别硬套。

6.2 框架中的观察者

框架 机制
Spring 事件机制 ApplicationEventPublisher + @EventListener,最贴近观察者的生产级实现
Servlet 监听器 ServletContextListener、HttpSessionListener
Log4j/Logback Appender 日志事件广播给多个 Appender
响应式编程(Reactor/RxJava) Flux 订阅模型是观察者的思想延伸
MQ(消息队列) 发布-订阅是观察者的"分布式放大版"(见第七节)

6.3 典型业务场景

  1. 订单状态变更 → 多渠道通知(短信/邮件/站内信/推送)------ 本文示例
  2. 库存扣减 → 预警 + 看板 + 日志
  3. 用户注册成功 → 发券 + 赠送积分 + 首封邮件
  4. 配置中心变更 → 各服务动态刷新
  5. 审批流推进 → 通知发起人/抄送人/管理员
  6. 实时协作/聊天室 → 一条消息广播给多个在线客户端
  7. 数据看板:指标变化 → 多个图表刷新

6.4 反向判断:什么时候不该用

说清楚边界,比会用更有价值。

不适用的情况 原因
只有一个观察者 用不着抽象,直接调用更简单
观察者数量固定且永不变化 动态订阅的价值用不上
通知有严格的同步/事务要求 观察者通知通常在状态变更后触发,难与事务边界对齐(可能用"事务同步"或 MQ 更合适)
观察者之间有复杂顺序依赖 观察者模式不定义顺序,硬塞会让设计变味
需要可靠投递、重试、削峰 这是 MQ 的职责,进程内观察者做不到

七、观察者 vs 发布-订阅(面试必考对比)

很多面试官会追问这个,务必分清。

复制代码
【观察者模式】(进程内、直连)
  Subject ────────────────▶ Observer
              直接持有引用,直接调用

【发布-订阅】(中间多了一个"事件通道")
  Publisher ──▶ Event Channel/Broker ──▶ Subscriber
                    (解耦了双方)
维度 观察者模式 发布-订阅
中间层 无,Subject 直接持有 Observer 引用 有,经由事件通道/Broker 中转
耦合度 双方仍相互感知(Subject 持有引用) 双方完全不感知
范围 进程内 可跨进程、跨服务、跨机器
同步性 通常同步(通知时直接调用) 通常异步
失败处理 需自己做异常隔离 由 MQ 提供重试/持久化/削峰
典型实现 本文代码 Kafka、RabbitMQ、Redis Pub/Sub

一句话记忆:

观察者是"直接喊人",发布订阅是"发到群里,谁在谁看"。

发布-订阅可以看作"观察者模式 + 中间 Broker"的演化。

面试加分说法 :MQ 的 Topic 订阅、Spring 的 @EventListener(同进程)分别落在两端------Spring 事件更接近观察者,MQ 更接近发布订阅。


八、优缺点

优点

  1. 松耦合:Subject 只依赖 Observer 接口,双方可独立变化
  2. 满足开闭原则:新增观察者无需修改 Subject
  3. 动态订阅:运行期自由增减,灵活
  4. 职责清晰:事件产生与事件响应彻底分离

缺点(务必背熟,面试一定会被追问)

  1. 通知顺序不确定:模式不保证观察者执行先后
  2. 可能触发连锁更新 :观察者里再改状态会引发新的通知 → 循环依赖/栈溢出风险
  3. 难以精确控制调用栈:异步化后更难排查"谁通知了谁"
  4. 测试复杂度上升:观察者多时,验证需要桩化(mock)多个对象
  5. 内存泄漏风险:观察者注册后忘记反注册,Subject 长期持有引用 → 对象无法回收
  6. 异常传染(不做隔离时):一个观察者抛异常打断整条通知链(见 5.2)

诚实标注 :第 6 点是我基于"通知循环未做 try-catch"的确定性推断,已在 5.2 实测证实;其余 1-5 点为该模式的通用共识,非本文实测。


九、面试:亮点分级 + 追问应答链

按你的要求诚实分级------别把"会背定义"当成亮点。

🟢 骨架级(大多数人能达到,不算亮点)

你能说:定义、四个角色、优点缺点、会写个 demo。

追问 1:观察者模式属于哪类模式?

对象行为型模式。

追问 2:核心解耦靠什么?

靠 List<Observer> + 面向接口编程,Subject 不依赖具体实现。

结论 :这些是底线,面试官不会因此加分。要说"我用了观察者模式",至少要过下面这层。


🟡 加分区(有真实工程经验,能开始加分)

你能说 :推/拉模式区别、异常隔离、线程安全、为什么不用 java.util.Observable。

追问 1:推模式和拉模式怎么选?

数据小而稳定、大家都需要 → 推;数据大、各取所需 → 拉。拿不准就推一个事件对象,观察者自己决定要不要再取。

追问 2:通知循环里一个观察者抛异常怎么办?

单独 try-catch 隔离 + 记日志。因为通知是广播,性质是"尽力送达",不该让单点故障扩散。

追问 3:并发场景怎么保证安全?

观察者列表读多写少,用 CopyOnWriteArrayList;或遍历前 List.copyOf() 拷贝一份。

追问 4 :JDK 自带的 Observable 能用吗?

不能 ,JDK 9 起 @Deprecated。它继承而非接口、要手动 setChanged()、线程不安全。实测编译就有警告。


🔴 深水区(能讲出踩过的坑 + 有边界的思考,明显加分)

你能说:反注册失效的静默 bug、循环依赖、观察者 vs 发布订阅、模式的适用边界。

追问 1(杀手题):你用观察者模式时踩过什么坑?

反注册失效 :detach(new Observer(...)) 传新实例、且没重写 equals,List.remove() 走引用相等 → 移除失败但不报错 ,静默地一直发通知(典型表现:"用户都退订了还在收短信")。解法:持有同一引用 / 重写 equals+hashCode / 用 Map<String, Observer> 按 key 管理。这条我实测过,三种写法同框验证过。

追问 2:观察者模式会不会导致循环通知?

会。A 通知 B,B 的更新里又改了自己状态并通知回 A → 无限递归。缓解办法:通知里加"变更版本号/时间戳"判重、把通知改成异步批量、或干脆用事件总线切断直接引用。(此问为通用推导,本文未实测)

追问 3:观察者和发布订阅区别?

观察者进程内直连 ,Subject 直接持有 Observer 引用;发布订阅中间多一个 Broker ,双方不感知,可跨进程,通常异步并带重试/持久化。一句话:观察者是直接喊人,发布订阅是发群里。

追问 4 :什么时候你会故意不用观察者模式?

① 只有一个观察者;② 需要严格事务边界(通知难和事务对齐);③ 需要可靠投递/重试/削峰(该用 MQ);④ 观察者之间有复杂顺序依赖(模式不定义顺序)。能说清"什么时候不用",比会用更显水平。

追问 5:生产环境你还会加什么?

① 异常隔离;② COW 或遍历拷贝;③ 反注册的正确姿势;④ 通知去抖/防风暴(版本号判重);⑤ 关键通知考虑异步化 + 落库兜底。④⑤ 属于通用工程建议,本文未实测。


🎯 一句话自评定位

你的情况 定位
能背出定义和四个角色 🟢 骨架级
能讲推/拉、异常隔离、COW 🟡 加分区
能主动讲反注册的坑并给出三种解法 🔴 深水区
能讲清"什么时候不用"和"循环通知怎么防" 🔴 深水区(上限)

十、速记卡(面试前 30 秒扫一遍)

复制代码
定义:一对多依赖,状态变化自动通知
角色:Subject(持有列表+遍历通知)/ Observer(update 接口)
本质:把硬编码的扇出调用 → 面向接口的广播
两种:推(塞数据)/ 拉(给信号自己取)
加固:异常隔离 + CopyOnWriteArrayList + 反注册姿势
坑:  detach(new X()) 没 equals → 静默失效(我实测过)
废弃:java.util.Observable,JDK 9 起 @Deprecated
对比:观察者=进程内直连;发布订阅=多一个 Broker、可跨进程
不用:单观察者 / 强事务 / 要可靠投递(用MQ) / 有顺序依赖

附录 A:实测原始输出(JDK 21.0.12.1,javac 编译通过)

复制代码
========== Demo0: 反注册的坑(实测翻车点) ==========
  [坑A] 用【新实例】反注册 + 未重写 equals → 移除失败
  [邮件]   wang@x.com 收到:Order{A000, 测试}
    ↑ 邮件仍然收到 = detach 没生效
  [坑B] 用【同一个引用】反注册 → 成功
    ↑ 邮件没收到 = detach 生效
  [坑C] 重写 equals/hashCode 后,新实例也能移除
    ↑ 邮件没收到 = equals 让 detach 生效

========== Demo1: 最小骨架(推模式) ==========
  [短信]   138****0001 收到:Order{A1001, 已支付}
  [邮件]   zhang@x.com 收到:Order{A1001, 已支付}
  --- 新增观察者后再发一次 ---
  [短信]   138****0001 收到:Order{A1001, 已发货}
  [邮件]   zhang@x.com 收到:Order{A1001, 已发货}
  [短信]   139****0002 收到:Order{A1001, 已发货}
  --- 用同一引用反注册后再发(邮件应消失) ---
  [短信]   138****0001 收到:Order{A1001, 已签收}
    ↑ 只剩短信收到,邮件已反注册

========== Demo2: 异常隔离(坏观察者不拖垮别人) ==========
  [短信]   138****0001 收到:Order{B2002, 退款中}
  [隔离] 观察者 坏观察者 异常被吞:连接池耗尽
  [邮件]   li@x.com 收到:Order{B2002, 退款中}

========== Demo3: 拉模式(观察者自己取数) ==========
  [拉模式] 自己去 subject 取到:Order{C3003, 已完成}
  [短信]   137****0003 收到:Order{C3003, 已完成}

========== Demo4: 并发通知(COW 遍历安全) ==========
  [短信]   user0 收到:Order{D4004, 并发更新}
  [短信]   user1 收到:Order{D4004, 并发更新}
  [短信]   user2 收到:Order{D4004, 并发更新}
  并发下无 ConcurrentModificationException

额外事实核验:

复制代码
Observable 类上注解: @java.lang.Deprecated(forRemoval=false, since="9")
Observer 接口上注解: @java.lang.Deprecated(forRemoval=false, since="9")
JDK 版本: 21.0.12.1

附录 B:完整可运行源码

单文件,保存为 ObserverDemo.java,javac ObserverDemo.java && java ObserverDemo 即可复现上面全部输出。

java 复制代码
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ObserverDemo {

    // ---------- 领域模型 ----------
    static class Order {
        final String id;
        String status;
        Order(String id, String status) { this.id = id; this.status = status; }
        @Override public String toString() { return "Order{" + id + ", " + status + "}"; }
    }

    // ---------- 观察者接口 ----------
    interface Observer {
        void update(Order order);   // 推模式:被观察者直接把数据塞给你
        String name();
    }

    // ================= 最小骨架(推模式) =================
    static class SimpleSubject {
        private final List<Observer> observers = new CopyOnWriteArrayList<>();
        private Order latest;

        public void attach(Observer o)   { observers.add(o); }
        public void detach(Observer o)   { observers.remove(o); }
        public void changeStatus(Order o) {
            this.latest = o;
            notifyAllObservers();
        }
        private void notifyAllObservers() {
            for (Observer o : observers) {
                o.update(latest);              // 推:直接给数据
            }
        }
    }

    // ================= 异常隔离 + 防御性遍历 =================
    static class SafeSubject {
        private final List<Observer> observers = new CopyOnWriteArrayList<>();
        private Order latest;

        public void attach(Observer o) { observers.add(o); }
        public void detach(Observer o) { observers.remove(o); }

        public void changeStatus(Order o) {
            this.latest = o;
            // 即使不用 COW,也先拷贝一份再遍历,避免遍历中增删
            for (Observer o2 : List.copyOf(observers)) {
                try {
                    o2.update(latest);
                } catch (Exception e) {
                    // 关键:单个观察者炸了,不能拖垮整条通知链
                    System.out.println("  [隔离] 观察者 " + o2.name() + " 异常被吞:" + e.getMessage());
                }
            }
        }

        public Order getLatest() { return latest; }   // 拉模式取数入口
    }

    // ---------- 具体观察者 ----------
    static class SmsObserver implements Observer {
        private final String who;
        SmsObserver(String who) { this.who = who; }
        public void update(Order order) {
            System.out.println("  [短信]   " + who + " 收到:" + order);
        }
        public String name() { return "短信(" + who + ")"; }
    }

    // 不重写 equals ------ 专门用来演示"反注册失效"的坑
    static class EmailObserver implements Observer {
        private final String who;
        EmailObserver(String who) { this.who = who; }
        public void update(Order order) {
            System.out.println("  [邮件]   " + who + " 收到:" + order);
        }
        public String name() { return "邮件(" + who + ")"; }
    }

    // 重写 equals/hashCode ------ 新实例也能被正确移除
    static class SafeEmailObserver implements Observer {
        private final String who;
        SafeEmailObserver(String who) { this.who = who; }
        public void update(Order order) {
            System.out.println("  [邮件✓]  " + who + " 收到:" + order);
        }
        public String name() { return "邮件✓(" + who + ")"; }
        @Override public boolean equals(Object o) {
            return o instanceof SafeEmailObserver e && e.who.equals(this.who);
        }
        @Override public int hashCode() { return who.hashCode(); }
    }

    // 会抛异常的坏观察者 ------ 用来验证隔离
    static class BrokenObserver implements Observer {
        public void update(Order order) { throw new IllegalStateException("连接池耗尽"); }
        public String name() { return "坏观察者"; }
    }

    // 拉模式观察者:只收到"有变化"的信号,自己回头取数据
    static class PullObserver implements Observer {
        private final SafeSubject subject;
        PullObserver(SafeSubject subject) { this.subject = subject; }
        public void update(Order ignored) {
            Order o = subject.getLatest();
            System.out.println("  [拉模式] 自己去 subject 取到:" + o);
        }
        public String name() { return "拉模式观察者"; }
    }

    public static void main(String[] args) {
        System.out.println("========== Demo0: 反注册的坑(实测翻车点) ==========");
        System.out.println("  [坑A] 用【新实例】反注册 + 未重写 equals → 移除失败");
        SimpleSubject s0 = new SimpleSubject();
        s0.attach(new EmailObserver("wang@x.com"));
        s0.detach(new EmailObserver("wang@x.com"));   // 同内容,但是新对象
        s0.changeStatus(new Order("A000", "测试"));
        System.out.println("    ↑ 邮件仍然收到 = detach 没生效");

        System.out.println("  [坑B] 用【同一个引用】反注册 → 成功");
        SimpleSubject s1 = new SimpleSubject();
        EmailObserver ref = new EmailObserver("wang@x.com");
        s1.attach(ref);
        s1.detach(ref);                               // 同一引用
        s1.changeStatus(new Order("A001", "测试"));
        System.out.println("    ↑ 邮件没收到 = detach 生效");

        System.out.println("  [坑C] 重写 equals/hashCode 后,新实例也能移除");
        SafeSubject s2 = new SafeSubject();
        s2.attach(new SafeEmailObserver("wang@x.com"));
        s2.detach(new SafeEmailObserver("wang@x.com")); // 新实例,靠 equals 命中
        s2.changeStatus(new Order("A002", "测试"));
        System.out.println("    ↑ 邮件没收到 = equals 让 detach 生效");

        System.out.println();
        System.out.println("========== Demo1: 最小骨架(推模式) ==========");
        SimpleSubject subject = new SimpleSubject();
        subject.attach(new SmsObserver("138****0001"));
        subject.attach(new EmailObserver("zhang@x.com"));
        Order o1 = new Order("A1001", "已支付");
        subject.changeStatus(o1);
        System.out.println("  --- 新增观察者后再发一次 ---");
        subject.attach(new SmsObserver("139****0002"));
        subject.changeStatus(new Order("A1001", "已发货"));
        System.out.println("  --- 用同一引用反注册后再发(邮件应消失) ---");
        EmailObserver mailRef = new EmailObserver("zhang@x.com");
        SimpleSubject s3 = new SimpleSubject();
        s3.attach(new SmsObserver("138****0001"));
        s3.attach(mailRef);
        s3.detach(mailRef);
        s3.changeStatus(new Order("A1001", "已签收"));
        System.out.println("    ↑ 只剩短信收到,邮件已反注册");

        System.out.println();
        System.out.println("========== Demo2: 异常隔离(坏观察者不拖垮别人) ==========");
        SafeSubject safe = new SafeSubject();
        safe.attach(new SmsObserver("138****0001"));
        safe.attach(new BrokenObserver());
        safe.attach(new EmailObserver("li@x.com"));
        safe.changeStatus(new Order("B2002", "退款中"));

        System.out.println();
        System.out.println("========== Demo3: 拉模式(观察者自己取数) ==========");
        SafeSubject pullSub = new SafeSubject();
        pullSub.attach(new PullObserver(pullSub));
        pullSub.attach(new SmsObserver("137****0003"));
        pullSub.changeStatus(new Order("C3003", "已完成"));

        System.out.println();
        System.out.println("========== Demo4: 并发通知(COW 遍历安全) ==========");
        SafeSubject conc = new SafeSubject();
        for (int i = 0; i < 3; i++) conc.attach(new SmsObserver("user" + i));
        Thread t = new Thread(() -> conc.changeStatus(new Order("D4004", "并发更新")));
        t.start();
        try { t.join(); } catch (InterruptedException ignored) {}
        System.out.println("  并发下无 ConcurrentModificationException");
    }
}

相关推荐
vipxieliang1 小时前
Actuator 与 Micrometer:生产级监控体系搭建
java·spring boot
Seven971 小时前
Java已死?社招第一次跳槽:Boss沟通505次,6周拿下8个offer
java
代码不停1 小时前
MyBatis进阶
spring boot·mybatis
一级新生2 小时前
LLM _ Agent _ Skill _ 专家 的关系与选型笔记
java·开发语言·人工智能·笔记·大模型·智能体·skill
文丑颜不良啊2 小时前
LeetCode1541~1560题目解析
java·leetcode
她说彩礼65万2 小时前
C# TaskCompletionSource
java·开发语言·c#
当战神遇到编程2 小时前
Java 基础入门(七):递归详解——执行原理、方法调用栈与经典练习
java
若亦_2 小时前
排序>>交换排序
java·数据结构·算法