代码上线就出事,黑锅总往身上背?那是 Java 里那些“你不知道”的细节在作祟!你确定完全掌握 Java 了吗?那些看似基础的操作背后,可能隐藏着你从未触及的深度。《你不知道的 Java》 专栏,专挖这些隐藏知识点,每日一个知识点,助你写出真正健壮、少出问题的代码,从此告别救火队员,安心下班!
本期 2.5W 字长文,强烈推荐收藏,随时查阅!力求讲清楚,从此对设计模式祛魅,结合自身经验告诉你设计模式真的很重要!另外最后附有设计模板公式,可直接复用。
说到设计模式,是不是感觉既熟悉又陌生?熟悉的是面试题里总有它的身影,陌生的是实际项目中好像用得不多,或者用了也不知道是它?尤其是观察者模式,听起来好像挺玄乎,什么“发布-订阅”、“依赖倒置”… 别怕,今天咱们就把它扒个精光,让它从“神坛”走下来,变成你我手中的利器!
1. 基础回忆
在咱们撸起袖子干观察者模式之前,先快速扫盲一下设计模式的三大家族,有个全局观总没错:
- 创建型模式 (Creational Patterns): 关注点是“怎么创建对象?”。
- 结构型模式 (Structural Patterns): 关注点是“怎么组合类和对象?”。它们的目标是让类和对象的组合更强大、更灵活。
- 行为型模式 (Behavioral Patterns): 关注点是“对象之间怎么交互和分配职责?”。它们的目标是让对象间的通信更清晰、更高效。咱们今天的主角——观察者模式就属于这个家族。
好了,热身完毕,现在正式进入主题:观察者模式 (Observer Pattern)!
2. 基础理论
2.1 什么是观察者模式?
想象一下你关注了某个技术大佬的公众号(比如我这个,咳咳…)。大佬(Subject/Observable,被观察者/主题)一发布新文章,微信(平台机制)就会自动通知所有关注了他的粉丝(Observer,观察者)。你作为粉丝,只需要做一件事:关注(Attach/Register)。等文章来了,你就收到通知,然后去阅读(Update)。如果哪天你不想关注了,取关(Detach/Unregister)就行。
核心思想: 定义对象间的一种 一对多 的依赖关系,当一个对象(Subject)的状态发生改变时,所有依赖于它的对象(Observers)都将得到 自动通知 并更新。
主要角色:
- Subject (主题/被观察者):
- 知道它的所有观察者。可以有任意多个观察者。
- 提供用于**添加(attach/register)和删除(detach/unregister)**观察者的接口。
- 状态改变时,**通知(notify)**所有注册的观察者。
- Observer (观察者):
- 定义一个**更新(update)**接口,当收到主题的通知时,这个接口会被调用。
- ConcreteSubject (具体主题):
- 存储具体状态。
- 当其状态改变时,向其注册的所有观察者发出通知。
- ConcreteObserver (具体观察者):
- 维护一个指向 ConcreteSubject 对象的引用(或者知道如何获取需要的数据)。
- 存储有关主题的状态,这些状态应与主题的状态保持一致。
- 实现 Observer 的更新接口,以使自身状态与主题的状态保持一致。
图示(简化版):
2.2 底层原理
观察者模式的精髓在于 解耦 (Decoupling)!
想象一下没有观察者模式的场景:大佬发文章,得自己维护一个粉丝列表(硬编码或者配置文件?),然后挨个去调用粉丝的“接收文章”方法。如果新增一个粉丝,或者某个粉丝的接收方式变了(比如从微信通知改成邮件),大佬就得改自己的代码!这耦合度,简直高得令人发指!大佬只想安安静静写文章,不想管你们这些粉丝的破事啊!
用了观察者模式后:
- 主题和观察者解耦: 主题只知道它有一堆实现了
Observer
接口的对象。它不关心这些对象具体是谁,是干嘛的。观察者也只知道它需要关注一个Subject
,并在收到通知时做自己的事。双方可以独立地变化和扩展。 - 易于扩展: 想增加新的观察者(比如,大佬发文后,除了通知粉丝,还想自动同步到微博)?只需要实现一个新的
Observer
类,并注册到主题上即可,完全不需要修改主题的代码。符合 开闭原则 (Open/Closed Principle)。 - 支持广播通信: 主题状态一变,所有观察者都能收到通知,非常适合需要“广而告之”的场景。
简单说,观察者模式就是用一种约定的方式(接口),让“被动方”(观察者)主动去“订阅”信息源(主题),而信息源在变化时,只需要按照约定去“广播”,不需要关心谁在听,怎么听。
3. 最佳实践
3.1 公式模版
Talk is cheap, show me the code! 咱们来看一个最经典的观察者模式实现结构:
import java.util.ArrayList;
import java.util.List;
// 1. 定义观察者接口
interface Observer {
void update(String message); // 当主题状态改变时,调用此方法
}
// 2. 定义主题接口 (或者抽象类)
interface Subject {
void attach(Observer observer); // 添加观察者
void detach(Observer observer); // 移除观察者
void notifyObservers(String message); // 通知所有观察者
}
// 3. 实现具体主题
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private String state; // 主题的状态
public String getState() {
return state;
}
// 状态改变时,通知所有观察者
public void setState(String state) {
this.state = state;
System.out.println("主题状态改变为: " + state);
notifyObservers("主题状态已更新为: " + state);
}
@Override
public void attach(Observer observer) {
if (!observers.contains(observer)) {
observers.add(observer);
System.out.println("观察者 " + observer.getClass().getSimpleName() + " 已添加.");
}
}
@Override
public void detach(Observer observer) {
if (observers.remove(observer)) {
System.out.println("观察者 " + observer.getClass().getSimpleName() + " 已移除.");
}
}
@Override
public void notifyObservers(String message) {
System.out.println("开始通知所有观察者...");
// 遍历通知 - 注意:迭代时直接删除可能引发 ConcurrentModificationException,实际应用中可能需要复制列表或使用迭代器安全删除
for (Observer observer : new ArrayList<>(observers)) { // 使用副本以避免并发修改问题
observer.update(message);
}
System.out.println("通知完毕.");
}
}
// 4. 实现具体观察者 A
class ConcreteObserverA implements Observer {
@Override
public void update(String message) {
System.out.println("ConcreteObserverA 收到通知: " + message + " -> 执行A逻辑");
// 这里可以做具体的操作,比如更新UI、记录日志等
}
}
// 5. 实现具体观察者 B
class ConcreteObserverB implements Observer {
@Override
public void update(String message) {
System.out.println("ConcreteObserverB 收到通知: " + message + " -> 执行B逻辑");
// 做其他操作
}
}
// 客户端代码 (演示如何使用)
public class ObserverDemo {
public static void main(String[] args) {
// 创建主题
ConcreteSubject subject = new ConcreteSubject();
// 创建观察者
Observer observerA = new ConcreteObserverA();
Observer observerB = new ConcreteObserverB();
// 注册观察者
subject.attach(observerA);
subject.attach(observerB);
System.out.println("\n--- 第一次状态变更 ---");
// 改变主题状态,观察者会自动收到通知
subject.setState("数据更新V1.0");
System.out.println("\n--- 移除观察者A ---");
subject.detach(observerA);
System.out.println("\n--- 第二次状态变更 ---");
// 再次改变主题状态
subject.setState("数据更新V2.0 - A已收不到");
}
}
运行结果大概是这样:
观察者 ConcreteObserverA 已添加.
观察者 ConcreteObserverB 已添加.
--- 第一次状态变更 ---
主题状态改变为: 数据更新V1.0
开始通知所有观察者...
ConcreteObserverA 收到通知: 主题状态已更新为: 数据更新V1.0 -> 执行A逻辑
ConcreteObserverB 收到通知: 主题状态已更新为: 数据更新V1.0 -> 执行B逻辑
通知完毕.
--- 移除观察者A ---
观察者 ConcreteObserverA 已移除.
--- 第二次状态变更 ---
主题状态改变为: 数据更新V2.0 - A已收不到
开始通知所有观察者...
ConcreteObserverB 收到通知: 主题状态已更新为: 数据更新V2.0 - A已收不到 -> 执行B逻辑
通知完毕.
看到没?主题只管改状态和喊一嗓子(notifyObservers
),观察者们自动响应。添加、移除观察者对主题来说也很简单。解耦!优雅!
理论成立,实践开始!
3.2 最佳实践
光看模板不过瘾,咱们上点实际工作中用得到的例子。
3.2.1 用户注册:短信通知与优惠券发放(同步/异步请求解耦)
场景: 用户注册成功后,核心流程是创建用户记录。但通常还需要:
- 发送注册成功短信。(即时,同步请求)
- 给用户发放新人优惠券。(可容忍延时,异步请求)
直接在注册逻辑里写发送短信和发券的代码?耦合太高!如果以后要加“发送欢迎邮件”、“初始化积分”呢?注册方法会越来越臃肿。
这里发送优惠劵不是那么要求即时性,那么大家可能首先会想到用 MQ 消息处理,将业务解耦,达到异步请求的目的。但其实这里也可以用观察者模式解决。
观察者模式解决方案:
- 主题 (Subject):
UserService
或UserRegistrationService
。它在用户成功注册后,触发一个“用户已注册”事件。 - 观察者 (Observers):
SmsNotifierObserver
: 监听“用户已注册”事件,收到通知后调用短信服务发送短信。CouponIssuerObserver
: 监听“用户已注册”事件,收到通知后调用优惠券服务发放优惠券。
代码示意 (核心逻辑):
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 事件对象 (可选,可以传递更丰富的数据)
class UserRegisteredEvent {
String userId;
String phoneNumber;
// ... 其他信息
public UserRegisteredEvent(String userId, String phoneNumber) {
this.userId = userId;
this.phoneNumber = phoneNumber;
}
// Getters...
}
// 观察者接口
interface UserRegistrationObserver {
void onUserRegistered(UserRegisteredEvent event);
}
// 主题:用户服务
class UserService {
private List<UserRegistrationObserver> observers = new ArrayList<>();
// 搞个线程池玩玩异步,更真实一点
private ExecutorService asyncExecutor = Executors.newFixedThreadPool(2);
public void addObserver(UserRegistrationObserver observer) {
observers.add(observer);
}
public void removeObserver(UserRegistrationObserver observer) {
observers.remove(observer);
}
public void registerUser(String username, String password, String phoneNumber) {
System.out.println("核心逻辑:正在注册用户 " + username + " ...");
// 假设这里是数据库操作等...
String userId = "user_" + System.currentTimeMillis(); // 模拟生成用户ID
System.out.println("用户 " + username + " 注册成功!ID: " + userId);
// 注册成功,发布事件
UserRegisteredEvent event = new UserRegisteredEvent(userId, phoneNumber);
notifyObservers(event);
System.out.println("注册流程主体完成。");
}
private void notifyObservers(UserRegisteredEvent event) {
System.out.println("开始通知注册成功的观察者...");
for (UserRegistrationObserver observer : observers) {
// 这里可以根据需要决定是同步调用还是异步调用
if (observer instanceof SmsNotifierObserver) {
// 短信可能比较重要,同步调用 (或者用更可靠的方式)
observer.onUserRegistered(event);
} else if (observer instanceof CouponIssuerObserver) {
// 发券可以容忍延迟,异步执行,不阻塞主注册流程
asyncExecutor.submit(() -> observer.onUserRegistered(event));
System.out.println("已将发券任务提交到异步线程池。");
} else {
// 其他观察者默认同步
observer.onUserRegistered(event);
}
}
// 注意:实际项目中,异步任务的异常处理、线程池管理等需要更完善
}
// 在应用关闭时记得关闭线程池
public void shutdownAsyncExecutor() {
asyncExecutor.shutdown();
}
}
// 观察者1:短信通知
class SmsNotifierObserver implements UserRegistrationObserver {
@Override
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("[短信服务] 收到通知 (线程: " + Thread.currentThread().getName() + ")");
System.out.println("[短信服务] 准备向手机 " + event.phoneNumber + " 发送注册成功短信...");
// 模拟调用短信API
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("[短信服务] 短信发送成功!");
}
}
// 观察者2:优惠券发放
class CouponIssuerObserver implements UserRegistrationObserver {
@Override
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("[优惠券服务] 收到通知 (线程: " + Thread.currentThread().getName() + ")");
System.out.println("[优惠券服务] 准备为用户 " + event.userId + " 发放新人优惠券...");
// 模拟调用优惠券服务
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 模拟耗时操作
System.out.println("[优惠券服务] 新人优惠券发放成功!");
}
}
// 测试
public class UserRegistrationDemo {
public static void main(String[] args) {
UserService userService = new UserService();
// 添加观察者
userService.addObserver(new SmsNotifierObserver());
userService.addObserver(new CouponIssuerObserver());
// 执行注册
userService.registerUser("Alice", "password123", "13800138000");
// 等待异步任务执行一会 (简单演示)
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
// 关闭线程池
userService.shutdownAsyncExecutor();
}
}
好处:
UserService
只负责核心注册逻辑和发布事件,非常干净。- 添加新功能(如发送邮件)只需新增
EmailNotifierObserver
并注册,无需修改UserService
。 - 异步处理:对于非核心、可延迟的操作(如发券),观察者的
update
方法可以将其提交到线程池异步执行,不阻塞主流程,提升用户体验。注意:观察者模式本身不提供异步机制,但它为实现异步处理提供了完美的切入点。 这比直接引入 MQ 更轻量级,适用于单体应用或服务内部的解耦。
3.2.2 模拟北京小客车指标摇号
场景: 摇号系统(主题)公布结果后,需要通知所有申请人(观察者)。
- 主题 (Subject):
LotterySystem
(摇号系统)。核心状态是本期摇号结果。 - 观察者 (Observer):
Applicant
(申请人)。每个人都关心自己是否中签。
代码示意:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
// 摇号结果
class LotteryResult {
int issueNumber; // 期号
List<String> winners; // 中签者名单
public LotteryResult(int issueNumber, List<String> winners) { this.issueNumber = issueNumber; this.winners = winners; }
// Getters...
}
// 观察者接口
interface LotteryObserver {
void onResultPublished(LotteryResult result);
}
// 主题:摇号系统
class LotterySystem {
private List<LotteryObserver> observers = new ArrayList<>();
private LotteryResult currentResult;
public void addObserver(LotteryObserver observer) { observers.add(observer); }
public void removeObserver(LotteryObserver observer) { observers.remove(observer); }
// 模拟进行摇号并公布结果
public void conductLottery(int issueNumber, List<String> allApplicants) {
System.out.println("\n--- 第 " + issueNumber + " 期摇号开始 ---");
// 模拟摇号逻辑...
Random random = new Random();
List<String> winners = new ArrayList<>();
int winnerCount = Math.min(3, allApplicants.size()); // 假设每期最多摇出3个
List<String> pool = new ArrayList<>(allApplicants);
for (int i = 0; i < winnerCount && !pool.isEmpty(); i++) {
int index = random.nextInt(pool.size());
winners.add(pool.remove(index));
}
System.out.println("摇号结束,中签名单:" + winners);
// 创建结果并通知
this.currentResult = new LotteryResult(issueNumber, winners);
notifyObservers();
System.out.println("--- 第 " + issueNumber + " 期摇号结果公布完毕 ---");
}
private void notifyObservers() {
System.out.println("开始通知所有申请人摇号结果...");
for (LotteryObserver observer : observers) {
observer.onResultPublished(currentResult);
}
}
}
// 观察者:申请人
class Applicant implements LotteryObserver {
private String name;
public Applicant(String name) { this.name = name; }
@Override
public void onResultPublished(LotteryResult result) {
System.out.print("[" + name + "] 收到第 " + result.issueNumber + " 期结果: ");
if (result.winners.contains(this.name)) {
System.out.println("恭喜!我中签了!激动的心,颤抖的手!");
} else {
System.out.println("唉,又没中,下期继续...");
}
}
public String getName() { return name; }
}
// 测试
public class LotteryDemo {
public static void main(String[] args) {
LotterySystem lotterySystem = new LotterySystem();
// 创建申请人 (观察者)
Applicant applicant1 = new Applicant("张三");
Applicant applicant2 = new Applicant("李四");
Applicant applicant3 = new Applicant("王五");
Applicant applicant4 = new Applicant("赵六");
List<String> allApplicantNames = List.of(applicant1.getName(), applicant2.getName(), applicant3.getName(), applicant4.getName());
// 申请人关注摇号结果
lotterySystem.addObserver(applicant1);
lotterySystem.addObserver(applicant2);
lotterySystem.addObserver(applicant3);
lotterySystem.addObserver(applicant4);
// 进行第一期摇号
lotterySystem.conductLottery(202401, allApplicantNames);
// 赵六不想摇了,取消关注
System.out.println("\n--- 赵六取消关注 ---");
lotterySystem.removeObserver(applicant4);
// 进行第二期摇号
lotterySystem.conductLottery(202402, List.of(applicant1.getName(), applicant2.getName(), applicant3.getName())); // 假设只有这三人继续
}
}
这个例子清晰地展示了“一对多”的通知机制。摇号系统不需要知道每个申请人的具体联系方式或通知偏好,只需要调用 observer.onResultPublished()
就行了。
3.2.3 java.util.Observable
和 java.util.Observer
Java 标准库里其实提供了观察者模式的实现:java.util.Observable
类和 java.util.Observer
接口。
但是,请注意:在现代 Java 开发中,通常不推荐使用它们!
为什么不推荐?
Observable
是一个类 (Class),而不是接口 (Interface): 这意味着你的主题类必须继承Observable
。Java 是单继承的,如果你的类已经继承了其他父类,那就没法用了,限制了灵活性。设计模式通常推荐“面向接口编程”,而不是“面向实现编程”。- API 设计问题:
setChanged()
方法:你需要先调用setChanged()
方法,然后调用notifyObservers()
才能真正发出通知。这个protected
的setChanged()
很容易被忘记调用,导致通知发不出去,排查问题时会很懵逼。notifyObservers(Object arg)
: 传递数据是通过一个Object
参数,不够类型安全,接收方需要进行类型转换。
- 线程安全问题:
Observable
中的Vector
(用于存储 observers) 是线程安全的,但其notifyObservers
方法的迭代过程并非完全安全,如果在迭代时有其他线程修改观察者列表(虽然 Vector 本身同步,但迭代逻辑不是原子性的),可能出现问题。更重要的是,你的update
方法和主题状态的改变需要你自己保证线程安全。
简单示例 (了解即可,不推荐使用):
import java.util.Observable; // 注意是类
import java.util.Observer; // 注意是接口
// 主题类,必须继承 Observable
class WeatherStation extends Observable {
private String weather;
public void setWeather(String weather) {
this.weather = weather;
System.out.println("气象站发布新天气: " + weather);
// 必须先调用 setChanged()
setChanged();
// 然后才能通知,可以传递数据
notifyObservers(weather);
}
}
// 观察者类,实现 Observer 接口
class PhoneDisplay implements Observer {
private String name;
public PhoneDisplay(String name) { this.name = name; }
@Override
public void update(Observable o, Object arg) { // 注意参数是 Observable 和 Object
if (o instanceof WeatherStation) {
String weather = (String) arg; // 需要类型转换
System.out.println("[" + name + " 手机显示] 天气更新: " + weather);
}
}
}
public class OldJdkObserverDemo {
public static void main(String[] args) {
WeatherStation station = new WeatherStation();
PhoneDisplay display1 = new PhoneDisplay("我的手机");
PhoneDisplay display2 = new PhoneDisplay("老婆的手机");
// 添加观察者
station.addObserver(display1);
station.addObserver(display2);
// 发布天气
station.setWeather("晴天,25度");
System.out.println("---");
station.setWeather("多云转阴,20度");
// 如果忘记 setChanged()
System.out.println("\n--- 忘记调用 setChanged() ---");
station.weather = "下雨了,15度"; // 直接修改状态(不推荐)
System.out.println("气象站状态改为: " + station.weather);
station.notifyObservers(station.weather); // 通知发不出去!
System.out.println("(可以看到,手机没有收到更新)");
// 正确做法还是通过 set 方法
System.out.println("\n--- 再次正确发布 ---");
station.setWeather("雨停了,彩虹!");
}
}
替代方案:
- 自己动手实现: 就像我们前面的模板代码一样,使用接口,更灵活。
java.beans.PropertyChangeSupport
和PropertyChangeListener
: 这是 JavaBeans 规范的一部分,更健壮,支持属性级别的监听。- 第三方库:
- Guava EventBus: Google Guava 库提供的强大的事件总线,解耦更彻底,使用注解,更方便。
- RxJava / Project Reactor: 面向响应式编程的库,观察者模式是其核心思想之一,功能更强大,适用于复杂的异步事件流处理。
- Spring Framework: Spring 的事件机制 (
ApplicationEvent
,ApplicationListener
) 也是观察者模式的一种应用。
更多例子
- GUI 事件监听: Swing/AWT/JavaFX 中的按钮点击、鼠标移动等事件处理,就是典型的观察者模式应用。
JButton
是主题,ActionListener
是观察者。 - MVC/MVP/MVVM 架构: Model(模型)是主题,View(视图)是观察者。当 Model 数据变化时,通知 View 更新显示。
4. 灵魂拷问(面试题)
面试官微微一笑,掏出了他的祖传问题…
问题 1:请解释一下什么是观察者模式,它的主要优点是什么?
解答:
观察者模式是一种 行为设计模式,它定义了对象之间的一种 一对多 的依赖关系。当一个对象(称为 主题 Subject 或 被观察者 Observable)的状态发生改变时,所有依赖于它的对象(称为 观察者 Observer)都会得到 自动通知 并被更新。
主要优点:
- 解耦 (Decoupling): 这是最核心的优点。主题和观察者之间是松耦合的。主题只知道观察者实现了某个接口,不关心其具体类型或内部逻辑。观察者也不知道主题的内部细节,只关心自己感兴趣的事件通知。这使得双方可以独立地变化和扩展。
- 易于扩展 (Extensibility): 可以很容易地增加新的观察者,而无需修改主题的代码,符合 开闭原则。
- 支持广播通信 (Broadcast Communication): 主题状态的改变可以方便地通知到所有相关的观察者。
- 符合依赖倒置原则 (Dependency Inversion Principle): 主题依赖于抽象的
Observer
接口,而不是具体的观察者类。
问题 2:JDK 自带的 java.util.Observable
和 Observer
有什么缺点?现在推荐使用什么替代方案?
解答:
JDK 自带的 java.util.Observable
和 Observer
虽然实现了观察者模式,但在现代 Java 开发中 不推荐使用,主要缺点有:
Observable
是类而非接口: 这强制要求主题类必须继承Observable
,限制了类的继承体系(Java 单继承),不够灵活。违反了“面向接口编程”的原则。- API 设计缺陷:
- 需要先调用
setChanged()
方法,然后notifyObservers()
才能生效,容易忘记调用setChanged()
导致通知失败。 notifyObservers(Object arg)
使用Object
传递数据,缺乏类型安全,需要强制类型转换。
- 需要先调用
- 潜在的线程安全问题: 虽然内部使用了
Vector
,但在某些并发场景下(如迭代时修改列表)仍可能存在问题,且需要开发者自行保证状态变更和update
方法的线程安全。
推荐的替代方案:
- 手动实现: 根据项目需求,自己编写基于接口的观察者模式实现(如本文开头的模板)。
java.beans.PropertyChangeSupport
/PropertyChangeListener
: JDK 内置的 JavaBeans 事件机制,更健壮,适用于属性监听。- 使用成熟的第三方库:
- Guava EventBus: Google Guava 库提供,基于注解,使用方便,解耦更彻底。
- RxJava / Project Reactor: 强大的响应式编程库,观察者模式是其基础,适合处理复杂的异步事件流。
- Spring Framework Event: 如果使用 Spring,其内置的事件机制 (
ApplicationEvent
,ApplicationListener
) 是很好的选择。
5. 扩展阅读:发布订阅模式
5.1 发布订阅 vs 观察者模式
扩展阅读,我们来详细聊聊观察者模式 (Observer Pattern) 和发布-订阅模式 (Publish-Subscribe Pattern, Pub/Sub) 之间的区别与联系。
它们俩确实非常相似,核心思想都是 解耦 消息的发送者(事件源)和接收者(处理者),实现一种 事件驱动 的机制。因为太像了,所以在很多场景下,大家会把它们混用,或者认为观察者模式就是一种简单的发布-订阅实现。
但严格来说,它们在 实现架构 和 耦合程度 上存在关键区别,尤其是在引入 消息中间件/事件总线 的场景下,发布-订阅模式的特征会更加明显。
主要的区别点在于 是否存在一个中心化的代理/中介(Broker/Event Bus):
-
观察者模式 (Observer Pattern):
- 直接通信: 主题 (Subject/Observable) 直接持有 观察者 (Observer) 的引用列表。当主题状态改变时,它会 直接遍历 这个列表,并调用每个观察者的
update
方法。 - 耦合关系: 主题和观察者之间虽然通过接口解耦了具体实现,但它们在运行时 直接知道对方的存在(至少主题知道观察者的接口,观察者通常也持有主题的引用或知道如何获取相关状态)。耦合度相对较高。
- 典型场景: 通常在 同一个应用程序或进程内部 使用。例如:
- 图形用户界面 (GUI) 事件监听(按钮点击 -> 监听器响应)。
- 模型-视图-控制器 (MVC) 架构中,模型 (Subject) 状态改变通知视图 (Observer) 更新。
- 我们之前例子中的用户注册后发送短信/优惠券(在同一个服务内部解耦)。
- 关注点: 主题关心的是“有哪些观察者需要被通知”,观察者关心的是“我关注的那个主题发生了变化”。
- 类比: 你直接订阅了某个杂志社的杂志(Subject),杂志社有你的地址(Observer 引用),每次出新刊就直接寄给你。
- 直接通信: 主题 (Subject/Observable) 直接持有 观察者 (Observer) 的引用列表。当主题状态改变时,它会 直接遍历 这个列表,并调用每个观察者的
-
发布-订阅模式 (Publish-Subscribe Pattern):
- 通过中介通信: 发布者 (Publisher) 和订阅者 (Subscriber) 之间 不直接 通信。它们都只与一个 中心化的消息代理 (Message Broker) 或事件总线 (Event Bus) 进行交互。
- 发布者: 将消息/事件发送到 Broker 的特定 主题 (Topic) 或 频道 (Channel),它不关心谁会接收这个消息。
- 订阅者: 向 Broker 订阅 自己感兴趣的 Topic/Channel,它不关心消息是谁发布的。
- Broker/Event Bus: 负责接收来自发布者的消息,并将这些消息 路由 给所有订阅了对应 Topic/Channel 的订阅者。
- 耦合关系: 发布者和订阅者之间是 完全解耦 的,它们互相不知道对方的存在,只依赖于中间的 Broker。耦合度非常低。
- 典型场景:
- 分布式系统、微服务架构: 服务间的异步通信,一个服务发布事件,多个其他服务可以订阅并处理。
- 消息队列 (MQ): 如 Kafka, RabbitMQ, RocketMQ 等都是典型的 Pub/Sub 实现。
- 应用程序内部的事件总线: 如 Guava EventBus, Spring ApplicationEvent 等,在单个应用内提供更彻底的解耦。
- 关注点: 发布者关心的是“把这个消息发到某个主题”,订阅者关心的是“订阅某个主题的消息”,Broker 关心的是“正确地路由消息”。
- 类比: 你去报刊亭(Broker)买报纸。报社(Publisher)把报纸送到报刊亭,你(Subscriber)去报刊亭买你感兴趣的报纸。报社和你不需要认识对方。
总结对比:
特性 | 观察者模式 (Observer) | 发布-订阅模式 (Publish-Subscribe) |
---|---|---|
核心区别 | 无 中心化 Broker/Event Bus | 有 中心化 Broker/Event Bus |
通信方式 | 主题 直接调用 观察者的 update 方法 | 发布者/订阅者 通过 Broker 间接通信 |
耦合关系 | 主题与观察者 直接 (基于接口) 耦合 | 发布者与订阅者 完全解耦 (只依赖 Broker) |
感知对方 | 主题知道观察者,观察者常知道主题 | 发布者与订阅者 互相不知道 对方存在 |
通信模型 | 通常是 同步 调用 (但可改为异步) | 通常是 异步 通信 (由 Broker 支持) |
典型范围 | 单个应用/进程内部 | 跨应用、跨进程、分布式系统,也可用于单应用内部解耦 |
联系:
- 观察者模式可以看作是发布-订阅模式的一种 简化实现 或 特定形式,尤其是在没有引入独立 Broker 的情况下。
- 核心思想都是 事件驱动 和 解耦。
- 在某些简单的、进程内的场景下,使用观察者模式(自己实现或用类似 Spring Event 的简单事件机制)可能比引入重量级的消息队列更合适。而对于需要跨服务通信、高可靠性、消息持久化、复杂路由的场景,则必须采用基于 Broker 的发布-订阅模式。
简单来说:观察者模式是“你若安好,便是晴天,我会直接告诉你”,发布-订阅模式是“我在世界中心呼唤爱(发给中介),谁爱听谁听(通过中介订阅)”。
5.2 发布订阅代码模版
我们来看一个经典的发布-订阅模式 (Publish-Subscribe Pattern) 的代码结构示例。
这个例子将实现一个简单的 事件总线 (Event Bus) 作为消息代理 (Broker)。发布者将事件发布到总线,订阅者向总线订阅特定类型的事件,总线负责将事件分发给对应的订阅者。
核心组件:
- Event (事件): 表示发生的某件事情,通常包含相关数据。可以是一个基类或接口。
- Publisher (发布者): 创建事件并将其发布到 Event Bus。它不直接与订阅者交互。
- Subscriber (订阅者): 对特定类型的事件感兴趣,并向 Event Bus 注册自己。它定义了处理事件的逻辑。它不直接与发布者交互。
- EventBus (事件总线 / 消息代理): 核心的中介。维护订阅关系(哪个订阅者对哪种事件感兴趣),接收发布者发布的事件,并将事件路由给所有相关的订阅者。
代码示例:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
// --- 1. 定义事件基类/接口 ---
// 可以是空接口作为标记,也可以包含通用字段(如时间戳)
interface Event {
long timestamp = System.currentTimeMillis();
default long getTimestamp() {
return timestamp;
}
}
// --- 具体事件类型 ---
class OrderPlacedEvent implements Event {
final String orderId;
final double amount;
public OrderPlacedEvent(String orderId, double amount) {
this.orderId = orderId;
this.amount = amount;
}
@Override
public String toString() {
return "OrderPlacedEvent [orderId=" + orderId + ", amount=" + amount + ", ts=" + getTimestamp() + "]";
}
}
class UserLoggedInEvent implements Event {
final String userId;
public UserLoggedInEvent(String userId) {
this.userId = userId;
}
@Override
public String toString() {
return "UserLoggedInEvent [userId=" + userId + ", ts=" + getTimestamp() + "]";
}
}
// --- 2. 定义订阅者接口 ---
// 使用泛型来指定感兴趣的事件类型,增强类型安全
interface Subscriber<T extends Event> {
void handleEvent(T event);
// 可以添加一个方法获取感兴趣的事件类型,或者在注册时指定
// Class<T> getEventType(); // 一种方式
}
// --- 3. 实现事件总线 (核心中介) ---
class SimpleEventBus {
// 维护订阅关系: 事件类型 -> 订阅者列表
// 使用 ConcurrentHashMap 和 CopyOnWriteArrayList 保证基本的线程安全
private final Map<Class<? extends Event>, List<Subscriber<?>>> subscribers = new ConcurrentHashMap<>();
/**
* 订阅事件
* @param eventType 感兴趣的事件类型 Class 对象
* @param subscriber 订阅者实例
* @param <T> 事件类型
*/
public <T extends Event> void subscribe(Class<T> eventType, Subscriber<T> subscriber) {
// 获取或创建该事件类型的订阅者列表
// computeIfAbsent 保证原子性操作
List<Subscriber<?>> subscriberList = subscribers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>());
subscriberList.add(subscriber);
System.out.println(subscriber.getClass().getSimpleName() + " subscribed to " + eventType.getSimpleName());
}
/**
* 取消订阅事件
* @param eventType 事件类型 Class 对象
* @param subscriber 订阅者实例
* @param <T> 事件类型
*/
public <T extends Event> void unsubscribe(Class<T> eventType, Subscriber<T> subscriber) {
List<Subscriber<?>> subscriberList = subscribers.get(eventType);
if (subscriberList != null) {
subscriberList.remove(subscriber);
System.out.println(subscriber.getClass().getSimpleName() + " unsubscribed from " + eventType.getSimpleName());
}
}
/**
* 发布事件
* @param event 要发布的事件实例
*/
public void publish(Event event) {
Class<?> eventType = event.getClass();
System.out.println("\nPublishing event: " + event.toString());
// 获取对该事件类型感兴趣的订阅者列表
List<Subscriber<?>> specificSubscribers = subscribers.get(eventType);
if (specificSubscribers != null && !specificSubscribers.isEmpty()) {
System.out.println("Notifying " + specificSubscribers.size() + " subscribers for " + eventType.getSimpleName());
for (Subscriber<?> subscriber : specificSubscribers) {
try {
// 这里需要进行类型转换,因为 Map 中存储的是 List<Subscriber<?>>
// 在 subscribe 方法的类型约束下,这里的转换理论上是安全的
@SuppressWarnings("unchecked") // 抑制未检查转换警告
Subscriber<Event> specificSubscriber = (Subscriber<Event>) subscriber;
specificSubscriber.handleEvent(event);
} catch (Exception e) {
// 实际应用中应添加异常处理逻辑,避免一个订阅者的失败影响其他订阅者
System.err.println("Error handling event in " + subscriber.getClass().getSimpleName() + ": " + e.getMessage());
}
}
} else {
System.out.println("No subscribers for " + eventType.getSimpleName());
}
// 可以选择性地通知订阅了所有事件(Event.class)的订阅者
// List<Subscriber<?>> allEventSubscribers = subscribers.get(Event.class);
// if (allEventSubscribers != null) { ... }
}
}
// --- 4. 实现具体订阅者 ---
class EmailService implements Subscriber<OrderPlacedEvent> {
@Override
public void handleEvent(OrderPlacedEvent event) {
System.out.println("[EmailService] Received OrderPlacedEvent: Sending confirmation email for order " + event.orderId + ".");
// 模拟发送邮件逻辑...
}
}
class InventoryService implements Subscriber<OrderPlacedEvent> {
@Override
public void handleEvent(OrderPlacedEvent event) {
System.out.println("[InventoryService] Received OrderPlacedEvent: Updating inventory for order " + event.orderId + ".");
// 模拟库存更新逻辑...
}
}
class AuditLogger implements Subscriber<UserLoggedInEvent> {
@Override
public void handleEvent(UserLoggedInEvent event) {
System.out.println("[AuditLogger] Received UserLoggedInEvent: Logging user login for userId " + event.userId + " at " + event.getTimestamp());
// 模拟记录日志...
}
}
// --- 5. 实现发布者 (不需要是特定类,任何需要发事件的地方都可以) ---
class OrderService {
private final SimpleEventBus eventBus;
public OrderService(SimpleEventBus eventBus) {
this.eventBus = eventBus;
}
public void placeOrder(String orderId, double amount) {
System.out.println("\n--- Placing Order: " + orderId + " ---");
// 核心业务逻辑...
System.out.println("Order " + orderId + " processing complete.");
// **发布事件到总线**
OrderPlacedEvent event = new OrderPlacedEvent(orderId, amount);
eventBus.publish(event); // 只与 EventBus 交互
}
}
class AuthenticationService {
private final SimpleEventBus eventBus;
public AuthenticationService(SimpleEventBus eventBus) {
this.eventBus = eventBus;
}
public void loginUser(String userId) {
System.out.println("\n--- User Logging In: " + userId + " ---");
// 核心认证逻辑...
System.out.println("User " + userId + " authenticated successfully.");
// **发布事件到总线**
UserLoggedInEvent event = new UserLoggedInEvent(userId);
eventBus.publish(event); // 只与 EventBus 交互
}
}
// --- 6. 演示如何使用 ---
public class PubSubDemo {
public static void main(String[] args) {
// 1. 创建事件总线实例 (全局唯一或按需注入)
SimpleEventBus eventBus = new SimpleEventBus();
// 2. 创建订阅者实例
EmailService emailService = new EmailService();
InventoryService inventoryService = new InventoryService();
AuditLogger auditLogger = new AuditLogger();
// 3. 订阅者向总线注册自己感兴趣的事件
eventBus.subscribe(OrderPlacedEvent.class, emailService);
eventBus.subscribe(OrderPlacedEvent.class, inventoryService);
eventBus.subscribe(UserLoggedInEvent.class, auditLogger);
System.out.println("\n--- Initial Subscriptions Set Up ---");
// 4. 创建发布者实例 (它们依赖 EventBus)
OrderService orderService = new OrderService(eventBus);
AuthenticationService authService = new AuthenticationService(eventBus);
// 5. 发布者执行业务操作,并发布事件 (发布者不知道谁在监听)
orderService.placeOrder("ORD123", 99.99);
authService.loginUser("user-alice");
// 6. 演示取消订阅
System.out.println("\n--- InventoryService Unsubscribing ---");
eventBus.unsubscribe(OrderPlacedEvent.class, inventoryService);
// 7. 再次发布同类型事件,看效果
orderService.placeOrder("ORD456", 150.50); // InventoryService 不会收到通知了
}
}
代码关键点说明:
- 解耦:
OrderService
和AuthenticationService
(发布者) 只依赖SimpleEventBus
,完全不知道EmailService
,InventoryService
,AuditLogger
(订阅者) 的存在。同样,订阅者也不知道是哪个服务发布的事件,它们只关心自己订阅的事件类型。 - 中心化的 EventBus:
SimpleEventBus
是核心,负责维护订阅关系和事件分发。这是与观察者模式最主要的区别(观察者模式中主题直接持有观察者引用)。 - 类型安全: 使用
Class<T>
和泛型Subscriber<T>
提高了类型安全,订阅者可以明确指定处理的事件类型,并在handleEvent
方法中直接使用该类型,减少了强制类型转换的需要(虽然在 EventBus 内部为了通用性进行了一次转换)。 - 线程安全: 示例中使用了
ConcurrentHashMap
和CopyOnWriteArrayList
来提供基本的线程安全,适用于多线程环境下的订阅/取消订阅和发布操作。CopyOnWriteArrayList
特别适合读多写少的场景(发布事件是读,订阅/取消是写)。 - 关注点分离: 每个组件职责清晰:发布者负责产生事件,订阅者负责处理事件,事件总线负责连接两者。
这个例子展示了发布-订阅模式的基本结构和优点,是构建更复杂事件驱动系统的基础。实际应用中,可能会使用更成熟的库(如 Guava EventBus、Spring ApplicationEvent、Akka Event Bus)或消息队列(Kafka, RabbitMQ 等)来实现,但核心的模式思想是一致的。
好了,今天关于观察者模式的“祛魅”就到这里。希望通过这次“庖丁解牛”,大家对它有了更清晰、更深入的认识。记住,设计模式不是银弹,也不是用来炫技的,而是前辈们总结出来的解决特定问题的“套路”。理解其原理,掌握其精髓,在合适的场景下灵活运用,才能真正提升我们的代码质量和开发效率。
下次遇到需要“一对多”通知、希望解耦模块的场景,别忘了咱们的观察者模式这位老朋友!
觉得有收获?点赞、收藏、转发走起!我们下期再见! Peace out! ✌️