GoF设计模式——观察者模式
为什么需要观察者模式假设在做一个气象监测系统一个气象站主题负责采集温度、湿度、气压多个展示面板观察者需要实时展示这些数据——一块在当前页面显示实时数值一块画历史曲线图还有一块做预警提示。最直觉的写法是在气象站里直接调用三个面板的更新方法class WeatherStation { public void dataChanged() { float temp readTemperature(); currentDisplay.update(temp); statisticsDisplay.update(temp); forecastDisplay.update(temp); } }这种写法的问题很快就会暴露每加一块新面板就要改WeatherStation气象站被迫认识所有面板面板想换名字、换类型气象站也跟着动。主题和观察者死死绑在一起牵一发而动全身。观察者模式解决的就是这个一个对象变了一堆对象要跟着变的耦合问题。主题只管说我变了谁关心谁自己来订阅互不认识。概念观察者模式Observer Pattern是一种行为型设计模式核心思想是定义一种一对多的依赖关系当一个对象主题的状态发生变化时所有依赖它的对象观察者都会自动收到通知并更新。业界常将其俗称为发布-订阅模式但严格来说两者结构有差异观察者模式中主题直接持有观察者引用并通知而通过消息中间件解耦的发布-订阅模式中发布者和订阅者互不相识。主题和观察者之间通过订阅-通知机制解耦主题只管通知我变了谁关心就自己来订阅主题不需要知道订阅者是谁观察者只管响应变化不需要知道是谁触发的。观察者模式涉及四个角色Subject主题接口定义注册、删除和通知观察者的方法通常包含一个状态状态变化时通知所有观察者Observer观察者接口定义更新方法接收主题通知时执行操作ConcreteSubject具体主题主题的具体实现维护一个观察者列表状态变化时遍历列表通知每个观察者ConcreteObserver具体观察者观察者的具体实现注册到主题中收到通知后执行业务逻辑实现实现持有观察者列表订阅«interface»Subjectattach(observer: Observer)detach(observer: Observer)notifyObserver()ConcreteSubject-observers: ListObserver-stategetState()setState()«interface»Observerupdate(subject: Subject)ConcreteObserver-nameupdate(subject: Subject)图中各类之间的关系Subject接口定义了attach、detach、notifyObserver三个方法ConcreteSubject实现该接口并持有ListObserverConcreteObserver实现Observer接口并在update方法中处理通知——主题和观察者之间只通过接口交互彼此不知道对方的具体类型。实现拉模型GoF 的标准实现采用拉模型notifyObserver遍历观察者列表并调用update将主题自身传给观察者——观察者在update中按需从主题拉取状态。这种模型让观察者有选择地获取数据而不是被动接收所有信息。// 主题接口 interface Subject { // 注册观察者 public void attach(Observer observer); // 移除观察者 public void detach(Observer observer); // 通知所有观察者 public void notifyObserver(); } // 观察者接口 interface Observer { // 接收主题通知按需拉取状态 public void update(Subject subject); } // 具体主题 class ConcreteSubject implements Subject { private String state; private ListObserver observers new ArrayList(); public void setState(String state) { this.state state; notifyObserver(); } public String getState() { return this.state; } public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void notifyObserver() { for (Observer ob : observers) { ob.update(this); } } } // 具体观察者 class ConcreteObserver implements Observer { private String name; public ConcreteObserver(String name) { this.name name; } public void update(Subject subject) { // 从主题拉取需要的状态 String state ((ConcreteSubject) subject).getState(); System.out.println(name 收到通知: state); } }可以把它想象成公共广播站广播站主题只负责喊一句现在播报最新消息并不关心谁在听、听哪些内容听众观察者自己决定要不要调到这个频道、听到消息后要做什么。广播站把话筒递给听众听众想听哪段就自己拿拉取状态。这个比喻贯穿后面的实现方便对照。实现思想通知时把主题对象本身交给观察者观察者按需从主题拉自己想要的状态。引入一个具体场景气象站采集温度三块显示面板都要展示。用拉模型实现主题把自身传给观察者面板各自从气象站拉取温度。// 主题接口 interface Subject { public void attach(Observer observer); public void detach(Observer observer); public void notifyObserver(); } // 观察者接口 interface Observer { // 拉模型把主题传进来观察者按需取数据 public void update(Subject subject); } // 具体主题气象站 class WeatherStation implements Subject { private float temperature; private ListObserver observers new ArrayList(); // 数据变化时由外部调用 public void setMeasurements(float temperature) { this.temperature temperature; notifyObserver(); // 状态一变就通知 } public float getTemperature() { return this.temperature; } public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void notifyObserver() { for (Observer ob : observers) { ob.update(this); // 把自身传给观察者 } } } // 具体观察者当前温度面板 class CurrentDisplay implements Observer { public void update(Subject subject) { // 从主题拉取温度 float temp ((WeatherStation) subject).getTemperature(); System.out.println(当前温度: temp ℃); } } // 具体观察者预警面板 class ForecastDisplay implements Observer { public void update(Subject subject) { float temp ((WeatherStation) subject).getTemperature(); // 只关心是否超过阈值按需取数据 System.out.println(temp 35 ? 高温预警 : 温度正常); } }关键点update(Subject subject)让观察者拿到整个主题对象可以按需获取任意状态。但代价是观察者需要知道具体主题类型这里用了强转存在一定耦合。强转这一步可以用泛型干掉——给Observer接口加一个类型参数把主题类型在编译期定下来// 主题接口 interface Subject { public void attach(Observer? extends Subject observer); public void detach(Observer? extends Subject observer); public void notifyObserver(); } // 观察者接口加泛型限定它观察的主题类型 interface ObserverT extends Subject { public void update(T subject); } // 具体观察者声明自己观察的是 WeatherStation class CurrentDisplay implements ObserverWeatherStation { public void update(WeatherStation station) { // 不需要强转直接调用具体主题的方法 float temp station.getTemperature(); System.out.println(当前温度: temp ℃); } }Spring 的ApplicationListenerE extends ApplicationEvent、Guava 的EventBus通过方法参数类型匹配观察者本质都是这个思路——把观察者关心哪种主题提前到编译期检查运行时就不用强转了。推模型推模型在通知时直接把数据传给观察者观察者不需要反查主题。// 观察者接口推模型 interface Observer { // 直接接收推送的数据 public void update(String state); } // 具体主题推模型 class ConcreteSubject implements Subject { private String state; private ListObserver observers new ArrayList(); public void setState(String state) { this.state state; notifyObserver(); } public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void notifyObserver() { for (Observer ob : observers) { // 直接推送数据给观察者 ob.update(state); } } } // 具体观察者推模型 class ConcreteObserver implements Observer { private String name; public ConcreteObserver(String name) { this.name name; } public void update(String state) { System.out.println(name 收到通知: state); } }推模型就像快递员送货上门广播站不再只喊来取件而是把包裹数据直接塞到每个听众手里听众不需要知道包裹来自哪个站点。实现思想通知时主题主动把数据推给观察者观察者不再依赖主题类型。同样用气象站场景推模型把温度直接塞给面板面板不需要强转主题类型。interface Observer { // 推模型主题直接把数据推过来 public void update(float temperature); } class WeatherStation implements Subject { private float temperature; private ListObserver observers new ArrayList(); public void setMeasurements(float temperature) { this.temperature temperature; notifyObserver(); } public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void notifyObserver() { for (Observer ob : observers) { ob.update(temperature); // 直接推送温度 } } } class CurrentDisplay implements Observer { public void update(float temperature) { // 不需要知道主题类型直接用数据 System.out.println(当前温度: temperature ℃); } }关键点推模型把数据直接塞给观察者观察者不需要知道主题的具体类型解耦更彻底。但如果主题的状态很多推模型可能传了观察者不需要的数据。如何选择拉模型和推模型各有适用场景维度拉模型传主题对象推模型传数据耦合度观察者依赖具体主题类型观察者只依赖数据类型灵活性观察者可按需获取任意状态只能获取推送的数据适用场景观察者需要频繁读取主题多个状态通知内容明确观察者不需要反查主题简单记忆数据少用推数据多用拉拿不准用拉。理由是推模型的接口签名很脆弱——气象站今天只推温度明天加了湿度、气压所有观察者的update签名都得跟着改拉模型只要在主题上加个getHumidity()观察者想用就取不想用不管。除非通知内容明确稳定比如就一个等级值否则优先选拉模型 泛型扩展性更好。总结观察者模式的本质是建立一对多的自动通知机制——一个对象变了所有依赖它的对象自动更新主题和观察者只通过接口交互。它和中介者、发布-订阅最大的不同在于主题只管广播我变了不关心谁在听观察者自己决定要不要订阅、收到通知后做什么。这种主题不关心订阅者的单向性是观察者模式区别于其他通信模式的精髓。什么时候用一个对象的状态变化需要通知其他对象且这些对象的数量、类型在编译时不确定希望在不修改主题的前提下动态增减观察者GUI 事件、数据绑定、消息推送等一变全变的场景什么时候不用观察者之间有严格的执行顺序要求观察者数量固定且永远不会变化简单的同步调用就能满足需求没必要引入观察者增加复杂度简单记忆主题广播我变了不关心谁在听观察者自己决定要不要听、听了做什么。⚠️ 用观察者模式要注意三个坑观察者注册后如果不注销主题会一直持有引用导致内存泄漏观察者在收到通知后又去修改主题状态可能触发连锁通知甚至无限循环通知过程中如果某个观察者直接调用detach注销自己会抛ConcurrentModificationException——修复方式是遍历时拷贝一份for (Observer ob : new ArrayList(observers))。相似模式区分观察者模式容易和中介者模式、发布-订阅模式混淆三者都涉及对象间通信但通信方向和解耦程度不同。总览对比模式接口关系核心意图典型场景观察者主题直接持有观察者引用一对多广播主题变了通知观察者GUI 事件、Spring Event、LiveData中介者同事对象只知道中介者多对多协调多个对象通过中介者交互聊天室、表单联动、航空管制发布-订阅发布者与订阅者互不相识通过中间件转发消息跨进程解耦Kafka、RabbitMQ、Redis Pub/Sub口诀观察者广播通知中介者统一调度发布订阅中间转发。观察者 vs 中介者观察者模式和中介者模式都涉及对象间的通信但方向不同观察者是一对多的广播主题状态变化通知所有观察者中介者是多对多的协调多个同事对象通过中介者交互避免彼此直接引用。维度观察者模式中介者模式核心意图一对多通知主题变了观察者跟着更新多对多协调多个对象通过中介者交互结构差异主题直接持有观察者列表中介者持有所有同事对象同事只知道中介者关注点状态变化的广播通知减少对象间的直接依赖典型场景GUI 事件、数据绑定、消息推送聊天室、表单联动、航空管制逐步区分法一个对象变了其他多个对象需要知道 → 选观察者模式多个对象之间互相影响直接引用会导致网状依赖 → 选中介者模式观察者 vs 发布-订阅观察者模式和发布-订阅模式经常被混用但有结构差异观察者模式中主题直接维护并通知观察者两者直接交互发布-订阅模式中发布者和订阅者通过消息中间件解耦发布者不知道订阅者的存在。维度观察者模式发布-订阅模式核心意图主题直接通知观察者通过中间件转发消息结构差异主题持有观察者引用直接调用发布者和订阅者无直接引用中间件转发关注点单进程/模块内的事件通知跨进程/跨系统的消息通信典型场景GUI 事件、Spring Event、LiveDataKafka、RabbitMQ、Redis Pub/Sub简单说观察者是直连发布-订阅是转发。大多数单体应用中的事件通知用观察者即可需要跨进程、跨系统解耦时用发布-订阅。练习题目智能家居警报系统题目描述小明家安装了智能家居警报系统。系统有一个中央警报器主题它会发布当前的警报等级1正常2预警3危险。家里有三种智能设备观察者它们对警报等级的响应各不相同警报灯Light当等级 ≥ 2 时亮起输出Light [名称]: ON否则输出Light [名称]: OFF警报器Siren当等级 3 时鸣响输出Siren [名称]: ALARM!否则输出Siren [名称]: silent显示屏Display任何等级都显示输出Display [名称]: Level [等级]设备可以随时被添加到系统或从系统中移除。被移除的设备不再收到通知。输入描述第一行是一个整数 N1 ≤ N ≤ 20表示初始设备数量。接下来的 N 行每行格式为TYPE NAME表示设备的类型和名称。接下来若干行每行是一条指令alert X将警报等级设为 X 并通知设备add TYPE NAME添加新设备remove NAME移除设备。最后一行为end。输出描述对每一条alert指令按设备注册顺序输出每个设备的响应每行一个。被移除的设备不输出。输入示例3 Light porch Siren hall Display panel alert 1 alert 3 add Light bedroom remove hall alert 2 end输出示例Light porch: OFF Siren hall: silent Display panel: Level 1 Light porch: ON Siren hall: ALARM! Display panel: Level 3 Light porch: ON Light bedroom: ON Display panel: Level 2解题思路题目中警报器Alarm是主题三种设备Light、Siren、Display是观察者。设备注册到Alarm中当调用alert X设置警报等级时Alarm的setState触发notifyObserver遍历所有已注册设备设备根据等级输出对应信息。add操作创建设备并注册到Alarmremove操作根据名称找到设备并移除。设备按注册顺序输出——这正是观察者模式主题状态变化自动通知所有观察者的体现。写完代码会发现初始设备注册和add操作中根据类型创建设备的if-else逻辑重复出现了两次。如果后续新增设备类型两处都要改容易遗漏。这时可以把创建逻辑收拢到一个方法中按类型返回对应的观察者——这就是简单工厂模式。进一步如果每种设备的创建逻辑变复杂可以将每个设备对应一个工厂类变成工厂方法模式。import java.util.*; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); int n sc.nextInt(); Alarm alarm new Alarm(0); // 读取初始设备并注册 while (n-- 0) { String type sc.next(); String name sc.next(); alarm.attach(createDevice(type, name)); } while (sc.hasNext()) { String op sc.next(); if (add.equals(op)) { String type sc.next(); String name sc.next(); alarm.attach(createDevice(type, name)); } else if (alert.equals(op)) { alarm.setState(sc.nextInt()); } else if (remove.equals(op)) { alarm.detach(sc.next()); } else { break; } } } // 简单工厂集中管理设备创建逻辑 static Observer createDevice(String type, String name) { switch (type) { case Light: return new Light(name); case Siren: return new Siren(name); default: return new Display(name); } } } // 主题接口 interface Subject { public void attach(Observer observer); public void detach(String name); public void notifyObserver(); } // 观察者接口 interface Observer { public void alert(int level); public String getName(); } // 具体主题中央警报器 class Alarm implements Subject { private int state; private ListObserver observers new ArrayList(); public Alarm(int state) { this.state state; } public void setState(int state) { this.state state; notifyObserver(); } public void attach(Observer observer) { observers.add(observer); } // 按名称移除观察者 public void detach(String name) { observers.removeIf(ob - ob.getName().equals(name)); } public void notifyObserver() { for (Observer ob : observers) { ob.alert(state); } } } // 具体观察者警报灯 class Light implements Observer { private String name; public Light(String name) { this.name name; } public String getName() { return name; } public void alert(int level) { System.out.println(Light name : (level 2 ? ON : OFF)); } } // 具体观察者警报器 class Siren implements Observer { private String name; public Siren(String name) { this.name name; } public String getName() { return name; } public void alert(int level) { System.out.println(Siren name : (level 3 ? ALARM! : silent)); } } // 具体观察者显示屏 class Display implements Observer { private String name; public Display(String name) { this.name name; } public String getName() { return name; } public void alert(int level) { System.out.println(Display name : Level level); } }代码中的createDevice方法就是简单工厂——把根据类型创建设备的逻辑集中到一个地方调用方只传类型和名称不需要关心具体怎么创建。如果要进一步扩展可以将每个设备的创建逻辑提取到独立的工厂类中变成工厂方法模式// 抽象工厂接口 interface DeviceFactory { public Observer create(String name); } // 具体工厂每个设备对应一个工厂类 class LightFactory implements DeviceFactory { public Observer create(String name) { return new Light(name); } } class SirenFactory implements DeviceFactory { public Observer create(String name) { return new Siren(name); } } class DisplayFactory implements DeviceFactory { public Observer create(String name) { return new Display(name); } }扩展实际项目中的观察者模式Spring ApplicationEventSpring 框架内置了观察者模式的实现通过ApplicationEvent和ApplicationListener实现事件驱动编程。订单创建后需要发通知、加积分、写日志如果把这些逻辑都塞在OrderService里服务会越来越臃肿。用 Spring Event 将这些关注点拆到独立的监听器中// 定义事件订单创建事件 public class OrderCreatedEvent extends ApplicationEvent { private final String orderId; private final BigDecimal amount; public OrderCreatedEvent(Object source, String orderId, BigDecimal amount) { super(source); this.orderId orderId; this.amount amount; } public String getOrderId() { return orderId; } public BigDecimal getAmount() { return amount; } } // 发布事件在订单服务中触发 Service public class OrderService { Autowired private ApplicationEventPublisher eventPublisher; public void createOrder(String orderId, BigDecimal amount) { // 订单创建逻辑... eventPublisher.publishEvent(new OrderCreatedEvent(this, orderId, amount)); } } // 监听事件发送通知 Component public class OrderNotificationListener implements ApplicationListenerOrderCreatedEvent { Override public void onApplicationEvent(OrderCreatedEvent event) { System.out.println(发送订单通知: event.getOrderId()); } } // 监听事件增加积分 Component public class OrderPointsListener implements ApplicationListenerOrderCreatedEvent { Override public void onApplicationEvent(OrderCreatedEvent event) { System.out.println(积分1: event.getOrderId()); } }关键点Spring Event 默认同步执行监听器按Order注解排序。如果需要异步处理在监听器方法上加Async。Spring 4.2 可以用EventListener注解替代实现接口的方式写法更简洁。Java PropertyChangeListenerJDK 自带的PropertyChangeListener是观察者模式的经典实现常用于 JavaBean 的属性变更通知。Swing 等 GUI 框架大量使用这套机制import java.beans.*; public class UserModel { // 封装观察者的注册和通知逻辑 private PropertyChangeSupport support new PropertyChangeSupport(this); private String name; public void setName(String name) { String oldName this.name; this.name name; // 只有值真正变化时才通知 support.firePropertyChange(name, oldName, name); } public String getName() { return name; } public void addPropertyChangeListener(PropertyChangeListener listener) { support.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener(listener); } } // 使用 UserModel user new UserModel(); user.addPropertyChangeListener(evt - { System.out.println(属性 evt.getPropertyName() 从 evt.getOldValue() 变为 evt.getNewValue()); }); user.setName(张三); // 触发通知关键点PropertyChangeSupport封装了观察者的注册和通知逻辑使用时不需要手动维护观察者列表。firePropertyChange会自动比较新旧值只有值真正变化时才通知——避免了无效通知。Guava EventBusGoogle Guava 提供的EventBus是对观察者模式的简化实现通过注解驱动不需要定义事件接口和观察者接口import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; public class EventBusDemo { public static void main(String[] args) { EventBus eventBus new EventBus(); // 注册观察者不需要实现接口方法上加 Subscribe 即可 eventBus.register(new Object() { Subscribe public void handleOrder(String orderId) { System.out.println(处理订单: orderId); } }); eventBus.register(new Object() { Subscribe public void notifyUser(String orderId) { System.out.println(通知用户: orderId); } }); // 发布事件通过方法参数类型匹配观察者 eventBus.post(ORD-001); } }关键点EventBus通过方法参数类型匹配事件不需要定义事件类一个Subscribe注解就搞定。如果需要异步处理用AsyncEventBus。适合简单的事件通知场景复杂场景需要事务、排序等建议用 Spring Event。RxJava 响应式扩展RxJava 将观察者模式扩展为响应式编程模型Observable被观察者/主题发射数据流Observer观察者订阅并处理。相比传统观察者模式RxJava 支持操作符对事件流进行变换和过滤import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observer; import io.reactivex.rxjava3.disposables.Disposable; import java.util.Arrays; public class RxJavaDemo { public static void main(String[] args) { // 创建被观察者主题 ObservableString observable Observable.fromIterable( Arrays.asList(订单创建, 支付成功, 发货通知) ); // 创建观察者 ObserverString observer new ObserverString() { Override public void onSubscribe(Disposable d) { System.out.println(开始监听); } Override public void onNext(String event) { System.out.println(收到事件: event); } Override public void onError(Throwable e) { System.out.println(发生错误: e.getMessage()); } Override public void onComplete() { System.out.println(所有事件处理完毕); } }; // 订阅建立观察关系 observable.subscribe(observer); } }关键点RxJava 的观察者模式支持背压Backpressure处理当事件产生速度超过消费速度时可以通过BackpressureStrategy控制。常用操作符map、filter、debounce等可以对事件流进行变换比传统观察者模式更强大。Android LiveDataAndroid Jetpack 的LiveData是生命周期感知的观察者模式实现解决了传统观察者模式在 Android 中的内存泄漏问题// ViewModel 中定义 LiveData public class UserViewModel extends ViewModel { private MutableLiveDataString userName new MutableLiveData(); public LiveDataString getUserName() { return userName; } public void loadUser() { // 模拟网络请求 userName.setValue(张三); } } // Activity 中观察 public class UserActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); UserViewModel viewModel new ViewModelProvider(this) .get(UserViewModel.class); // LifecycleOwner 确保 Activity 销毁时自动取消订阅 viewModel.getUserName().observe(this, name - { textView.setText(name); }); viewModel.loadUser(); } }关键点LiveData绑定LifecycleOwner后当 Activity/Fragment 销毁时自动移除观察者避免内存泄漏。setValue只能在主线程调用子线程需要用postValue。这是 Android 官方推荐的数据观察方式。四个框架放一起对比四个框架本质都是观察者模式但各自补足了不同的能力短板。用同一个场景订单创建后发通知 加积分跑一遍差异一目了然能力Spring EventGuava EventBusRxJavaLiveData同步/异步默认同步Async异步默认同步AsyncEventBus异步线程调度灵活subscribeOn/observeOn主线程生命周期管理无需手动注销无需手动unregister无需手动dispose✅ 自动取消订阅操作符/流变换无无✅map/filter/debounce等无需配合 Transformations典型场景Spring 应用内的领域事件单 JVM 内简单事件总线异步数据流、复杂事件编排Android UI 数据绑定