什么是观察者模式?应用场景有哪些?
面试考察点
-
设计思想理解:面试官不仅仅是想知道 "被观察者变了,观察者自动收到通知" 这句话,更是想看你是否理解 "一对多的依赖关系" 以及 "松耦合" 这个核心设计目标。
-
推模式 vs 拉模式:能否区分这两种数据传递方式,以及各自的优缺点。这属于进阶考点,答好了能加分。
-
框架实战关联:能否把观察者模式和 Spring Event、Guava EventBus、JDK
Observable等实际技术联系起来,证明你不只是纸上谈兵。
核心答案
一句话定义:观察者模式定义了对象之间 "一对多" 的依赖关系——当一个对象(被观察者)的状态发生变化时,所有依赖于它的对象(观察者)都会自动收到通知并更新。
打个比方:微信群就是一个典型的观察者模式。群里有人发消息(被观察者状态变化),所有群成员(观察者)都能立刻收到通知。你作为群成员不需要主动去问 "有人发新消息了吗",消息会自动推送到你面前。
上面的图展示了观察者模式的核心结构:
Subject(被观察者/主题):维护一个观察者列表,提供注册(attach)、注销(detach)和通知(notify)三个核心方法。状态变化时负责通知所有注册的观察者。Observer(观察者):定义一个统一的更新接口(update),收到通知后执行自己的业务逻辑。- 核心关系:一个被观察者对应多个观察者,这就是 "一对多"。被观察者不知道观察者的具体实现,只管调用
update(),实现了完全解耦。
深度解析
一、手写观察者模式
先用代码把核心结构写出来,一看就明白了。
// 观察者接口
public interface Observer {
void update(String message);
}
// 被观察者(主题)
public class Subject {
private List<Observer> observers = new ArrayList<>();
private String state;
// 注册观察者
public void attach(Observer observer) {
observers.add(observer);
}
// 注销观察者
public void detach(Observer observer) {
observers.remove(observer);
}
// 通知所有观察者
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(state);
}
}
// 状态变更时自动通知
public void setState(String state) {
this.state = state;
System.out.println("被观察者状态变更:" + state);
notifyObservers();
}
}
定义几个具体的观察者:
// 观察者 A:邮件通知
public class EmailObserver implements Observer {
@Override
public void update(String message) {
System.out.println("[邮件] 收到通知:" + message);
}
}
// 观察者 B:短信通知
public class SmsObserver implements Observer {
@Override
public void update(String message) {
System.out.println("[短信] 收到通知:" + message);
}
}
// 观察者 C:App 推送
public class AppObserver implements Observer {
@Override
public void update(String message) {
System.out.println("[App 推送] 收到通知:" + message);
}
}
使用:
Subject subject = new Subject();
// 注册观察者
subject.attach(new EmailObserver());
subject.attach(new SmsObserver());
subject.attach(new AppObserver());
// 状态变更,自动通知所有观察者
subject.setState("订单已发货");
输出:
被观察者状态变更:订单已发货
[邮件] 收到通知:订单已发货
[短信] 收到通知:订单已发货
[App 推送] 收到通知:订单已发货
看到没?Subject 完全不知道有哪些观察者、它们具体做什么。它只管调用 update(),至于发邮件、发短信还是 App 推送,那是观察者自己的事。新增通知渠道只需要写一个实现 Observer 的类,然后 attach 上去就行,被观察者的代码一行不用改。这就是松耦合的魅力。
二、推模式 vs 拉模式
这是面试中的一个细节考点。观察者模式有两种数据传递方式:
| 对比项 | 推模式(Push) | 拉模式(Pull) |
|---|---|---|
| 数据流向 | 被观察者主动把数据推给观察者 | 被观察者只通知 "变了",观察者自己来拉数据 |
update() 参数 | update(String data) | update(Subject subject) |
| 优点 | 观察者不用关心细节,拿来就用 | 观察者按需获取,更灵活 |
| 缺点 | 可能推送观察者不需要的数据 | 观察者需要了解被观察者的内部结构 |
上面手写的代码就是推模式——直接把 state 传给观察者。如果改成拉模式,update() 的参数就是 Subject 本身,观察者自己去调用 subject.getState() 获取需要的数据。
实际开发中推模式用得更多,因为耦合更低。
三、Spring Event 中的观察者模式
Spring 内置了一套事件机制,本质就是观察者模式。这比你自己手写要优雅得多。
// 1. 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
private String orderId;
public OrderCreatedEvent(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
// 2. 发布事件(被观察者)
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher publisher;
public void createOrder(String orderId) {
System.out.println("创建订单:" + orderId);
// 发布事件,通知所有监听器
publisher.publishEvent(new OrderCreatedEvent(this, orderId));
}
}
// 3. 监听事件(观察者 A:发邮件)
@Component
public class EmailNotifier {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("[邮件] 订单 " + event.getOrderId() + " 已创建,发送邮件通知");
}
}
// 4. 监听事件(观察者 B:扣库存)
@Component
public class StockService {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("[库存] 订单 " + event.getOrderId() + " 已创建,扣减库存");
}
}
调用 orderService.createOrder("ORD001") 后输出:
创建订单:ORD001
[邮件] 订单 ORD001 已创建,发送邮件通知
[库存] 订单 ORD001 已创建,扣减库存
Spring Event 的好处是:发布者和监听者完全解耦,通过 @EventListener 注解就能声明观察者,不用手动维护观察者列表。而且支持异步事件,加上 @Async 注解就行。
需要注意,Spring Event 默认是同步的——publishEvent() 会阻塞等待所有监听器执行完毕。如果你不希望主流程被拖慢,记得加 @Async。
四、Guava EventBus
如果项目没用到 Spring,或者想在非 Spring 环境用观察者模式,Google 的 Guava 提供了一个轻量级的事件总线——EventBus,用起来非常舒服。
// 创建事件总线
EventBus eventBus = new EventBus();
// 注册观察者
eventBus.register(new EmailListener());
eventBus.register(new SmsListener());
// 发布事件
eventBus.post("订单已发货");
观察者只需要在方法上加 @Subscribe 注解:
public class EmailListener {
@Subscribe
public void onEvent(String message) {
System.out.println("[邮件] " + message);
}
}
比手写观察者模式简洁多了,而且支持异步(AsyncEventBus)。
五、实际应用场景
| 场景 | 说明 |
|---|---|
| 事件驱动系统 | 下单后触发发邮件、扣库存、发短信等一系列操作 |
| 消息推送 | 用户关注某个话题后,有新内容自动推送 |
| GUI 事件监听 | 按钮点击、输入框内容变化等事件的响应 |
| 日志/监控 | 系统状态变更后自动记录日志、触发告警 |
| Spring 事件机制 | @EventListener 监听各种 Spring 事件 |
| RxJava/Reactor | 响应式编程的核心就是观察者模式 |
| ZooKeeper Watcher | 节点数据变更时自动通知客户端 |
六、观察者模式的注意事项
生产环境用观察者模式有几个坑要踩:
- 内存泄漏:观察者注册后忘记注销,导致被观察者一直持有观察者的引用,GC 回收不了。Spring Event 用
@Component管理生命周期还好,手写的话尤其要注意。 - 通知顺序不确定:如果观察者之间有先后依赖,手写模式不保证顺序。需要顺序的话用有序集合或者直接用 Spring 的
@Order注解。 - 级联触发:观察者 A 的
update()又触发了事件,导致观察者 B 被通知,B 又触发事件……形成循环调用。这在复杂系统里是个大坑。
面试高频追问
-
观察者模式和发布订阅模式是一回事吗?
- 严格来说不是。经典观察者模式中,被观察者直接通知观察者(彼此知道对方)。发布订阅模式引入了一个 "事件总线" 作为中间层,发布者和订阅者完全不知道对方的存在,解耦更彻底。Spring Event 和 Guava EventBus 更偏向发布订阅模式。
-
Spring Event 是同步还是异步的?
- 默认同步。
publishEvent()会等所有@EventListener执行完才返回。想要异步,在监听方法上加@Async,并确保项目中开启了@EnableAsync。
- 默认同步。
-
观察者模式怎么处理异常?
- 一个观察者抛异常不应该影响其他观察者。
notifyObservers()里需要给每个观察者的update()加try-catch,单独处理。Spring Event 也是这样,某个监听器报错不会阻断其他监听器。
- 一个观察者抛异常不应该影响其他观察者。
常见面试变体
- "手写一个观察者模式"
- "Spring 事件机制的原理是什么?"
- "观察者模式和发布订阅模式有什么区别?"
- "你在项目中哪里用到了观察者模式?"
记忆口诀
核心思想:一变多变,自动通知;注册注销,松耦合。
推 vs 拉:推模式把数据塞给你,拉模式你自己来拿。
与发布订阅的区别:观察者直连,发布订阅隔了个中间人。
总结
观察者模式的精髓就是 "一对多依赖,状态变化自动通知"。面试时先说概念,手写一个简单实现,然后重点展开 Spring Event 的用法,最后提一嘴推/拉模式和发布订阅的区别。能把 Spring Event 讲明白的候选人,面试官基本就认定你是有实战经验的。
