观察者模式是一种行为型设计模式,核心思想是建立一对多的依赖关系:当一个对象的状态发生改变,所有依赖它的对象都会收到通知并自动更新。在事件通知机制里,这个被依赖的对象称为主题或发布者,依赖方称为观察者或订阅者。通过把通知逻辑从业务代码里抽离出来,我们可以让新增监听者不需要修改主题源码,从而降低模块间的耦合度。

一、核心角色与接口设计
要实现一套通用的事件通知机制,首先需要定义两个基础抽象:主题(Subject)和观察者(Observer)。主题负责维护观察者列表,提供注册、注销和通知方法;观察者则声明一个更新接口,用于接收主题传递的事件数据。这种面向接口编程的方式,使得主题不需要关心观察者的具体类型,只依赖抽象契约。
在很多初学者实现中,容易把主题写成具体类并直接调用观察者的业务方法,这会让主题重新陷入耦合。正确的做法是将更新方法抽象为统一接口,例如update(event),其中event可以是普通对象或自定义事件类。这样无论是日志监听、界面刷新还是消息推送,都能以相同机制接入。下面的Java代码展示了基础接口定义。
// 观察者接口
public interface Observer {
void update(String event);
}
// 主题接口
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers(String event);
}
二、Java中的完整实现示例
基于上述接口,我们可以用一个具体主题类来管理观察者集合。通常内部使用CopyOnWriteArrayList或在通知时加锁,以避免在遍历列表时因并发修改抛出异常。主题在业务状态变化点调用notifyObservers,把事件逐个推送给观察者。下面的实现采用同步遍历,逻辑清晰,适合大多数后台服务场景。
需要注意,如果某个观察者的update方法执行缓慢,会阻塞后续观察者接收通知。因此在生产环境中,往往将通知逻辑提交到线程池异步执行。以下代码演示了基础同步版本,并附带了简单的异常隔离,防止单个观察者出错导致整体通知中断。
import java.util.ArrayList;
import java.util.List;
public class EventCenter implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers(String event) {
for (Observer o : new ArrayList<>(observers)) {
try {
o.update(event);
} catch (Exception e) {
System.err.println("观察者处理失败: " + e.getMessage());
}
}
}
}
// 使用示例
public class LogObserver implements Observer {
@Override
public void update(String event) {
System.out.println("日志模块收到事件: " + event);
}
}
三、JavaScript中的轻量实现
在前端或Node.js环境里,事件通知机制经常用更动态的方式表达。由于JS天生支持对象与函数作为一等公民,我们可以让主题直接以对象加数组保存回调函数,而不必强制定义接口类。这种写法更简洁,也贴合浏览器中addEventListener的使用直觉。
下面的实现提供了一个EventEmitter风格的迷你类,支持按事件名注册多个监听函数,并能在触发时传递任意参数。它演示了观察者模式在弱类型语言里的自然形态,同时保留了注册与注销能力,方便组件销毁时清理监听避免内存泄漏。
class EventEmitter {
constructor() {
this.listeners = {};
}
on(type, fn) {
if (!this.listeners[type]) this.listeners[type] = [];
this.listeners[type].push(fn);
}
off(type, fn) {
const arr = this.listeners[type];
if (arr) this.listeners[type] = arr.filter(item => item !== fn);
}
emit(type, data) {
const arr = this.listeners[type] || [];
arr.slice().forEach(fn => {
try { fn(data); } catch (e) { console.error(e); }
});
}
}
// 使用
const bus = new EventEmitter();
const handler = (msg) => console.log("收到:", msg);
bus.on("login", handler);
bus.emit("login", "用户A登录");
bus.off("login", handler);
四、推送与拉取两种通知策略
观察者模式在事件通知时有两种常见数据交互方式。一种是主题在通知时直接把事件对象推送给观察者,称为推模型;另一种是主题只告诉观察者“有变化”,观察者主动调用主题提供的getter方法获取自己关心的数据,称为拉模型。推模型实现简单、实时性好,但可能造成观察者收到不需要的数据。
拉模型把获取数据主动权交给观察者,主题接口更稳定,不会因扩展字段频繁改动。实际项目中可混合使用:主题推送一个轻量事件标识,观察者再按需拉取详情。下表对比了二者差异,帮助在编写时做取舍。
| 对比维度 | 推模型 | 拉模型 |
|---|---|---|
| 耦合方向 | 主题需知道数据细节 | 观察者需知道主题接口 |
| 网络或调用开销 | 一次传递完成 | 多次方法调用 |
| 扩展性 | 新增字段要改通知参数 | 主题结构变化影响小 |
五、异步与内存泄漏避坑
当事件通知涉及IO或复杂计算,同步遍历观察者会拖慢主题所在线程。此时应将每个update提交到独立线程或消息队列。在Java中可用ExecutorService包装通知循环;在JS中可用setTimeout或Promise延迟执行。这样主题发布事件后立即返回,提升响应速度。
另一个常见问题是内存泄漏:如果主题生命周期长于观察者(如全局事件总线),而观察者未显式注销,垃圾回收器无法释放观察者实例。在Java里可用WeakReference存储观察者,在JS里应在组件卸载时调用off。养成注册必注销的习惯,才能保证事件通知机制长期稳定运行。
六、总结与实践建议
编写观察者模式的事件通知机制,关键在于抽象出统一的观察者接口和主题管理方法,并根据语言特性选择实现形态。Java适合用接口与线程安全集合构建严谨的发布订阅中心;JavaScript适合用事件名映射回调的灵活发射器。无论哪种,都要考虑异常隔离、异步化和注销清理。
建议在项目初期就把事件总线或主题基类抽离成独立模块,业务代码只关心注册监听与发布事件。这样后续接入监控、埋点、跨模块通信都只需新增观察者,不需要改动原有逻辑,真正发挥观察者模式解耦与扩展的优势。