【设计模式】观察者模式——事件通知机制

一、从一个问题开始

假设你正在开发一个天气预报系统,当天气数据更新时,需要通知多个显示设备:

java

csharp 复制代码
public class WeatherData {
    private float temperature;
    private float humidity;
    private float pressure;
    
    // 问题:每次新增显示设备,都要修改这个类
    private CurrentConditionsDisplay currentDisplay;
    private StatisticsDisplay statisticsDisplay;
    private ForecastDisplay forecastDisplay;
    
    public void measurementsChanged() {
        currentDisplay.update(temperature, humidity, pressure);
        statisticsDisplay.update(temperature, humidity, pressure);
        forecastDisplay.update(temperature, humidity, pressure);
        // 每加一个设备,都要加一行调用
    }
}

问题

问题 说明
紧耦合 天气数据和显示设备直接绑定
违反开闭原则 新增显示设备要修改核心类
难以复用 显示设备无法在其他地方使用
难以测试 无法单独测试显示设备

观察者模式的解决方案 :天气数据作为被观察者(主题) ,显示设备作为观察者,两者通过接口解耦。

二、观察者模式是什么?

一句话定义 :定义对象间的一种一对多依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都得到通知并自动更新。

通俗理解

  • 就像微信公众号:你关注了公众号(订阅),它发文章时你会收到推送
  • 就像股票交易:你订阅某只股票的行情,价格变化时会收到通知
  • 就像QQ群消息:你在群里,群友发消息你都能收到

角色结构

text

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      Subject(主题接口)                      │
│  + registerObserver(Observer)                               │
│  + removeObserver(Observer)                                 │
│  + notifyObservers()                                        │
└─────────────────────────────────────────────────────────────┘
                              △
                              │ 实现
┌─────────────────────────────────────────────────────────────┐
│                    ConcreteSubject(具体主题)                │
│  - observers: List<Observer>                                │
│  - state                                                     │
│  + 业务方法                                                  │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ 通知
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Observer(观察者接口)                   │
│                     + update(state)                         │
└─────────────────────────────────────────────────────────────┘
                              △
                              │ 实现
              ┌───────────────┼───────────────┐
              │               │               │
      ┌───────┴───────┐ ┌─────┴──────┐ ┌───────┴───────┐
      │ ConcreteObs A │ │ConcreteObs B│ │ConcreteObs C │
      └───────────────┘ └────────────┘ └───────────────┘

三、代码实现:天气预报系统

3.1 观察者模式基础实现

java

csharp 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 观察者接口
 */
public interface Observer {
    void update(float temperature, float humidity, float pressure);
}

/**
 * 主题接口
 */
public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

/**
 * 具体主题:天气数据
 */
public class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;
    
    public WeatherData() {
        observers = new ArrayList<>();
    }
    
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
        System.out.println("新增观察者,当前观察者数量:" + observers.size());
    }
    
    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
        System.out.println("移除观察者,当前观察者数量:" + observers.size());
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }
    
    /**
     * 当天气数据更新时,自动通知所有观察者
     */
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
    
    public void measurementsChanged() {
        notifyObservers();
    }
}

/**
 * 具体观察者:当前天气显示
 */
public class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;
    private String name;
    
    public CurrentConditionsDisplay(String name, Subject weatherData) {
        this.name = name;
        weatherData.registerObserver(this);
    }
    
    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    public void display() {
        System.out.println(name + " - 当前天气:温度 " + temperature + "°C,湿度 " + humidity + "%");
    }
}

/**
 * 具体观察者:统计显示
 */
public class StatisticsDisplay implements Observer {
    private List<Float> temperatures = new ArrayList<>();
    private String name;
    
    public StatisticsDisplay(String name, Subject weatherData) {
        this.name = name;
        weatherData.registerObserver(this);
    }
    
    @Override
    public void update(float temperature, float humidity, float pressure) {
        temperatures.add(temperature);
        display();
    }
    
    public void display() {
        float avg = (float) temperatures.stream().mapToDouble(Float::doubleValue).average().orElse(0);
        System.out.println(name + " - 平均温度:" + avg + "°C");
    }
}

/**
 * 具体观察者:天气预报
 */
public class ForecastDisplay implements Observer {
    private float lastPressure;
    private float currentPressure;
    private String name;
    
    public ForecastDisplay(String name, Subject weatherData) {
        this.name = name;
        weatherData.registerObserver(this);
    }
    
    @Override
    public void update(float temperature, float humidity, float pressure) {
        lastPressure = currentPressure;
        currentPressure = pressure;
        display();
    }
    
    public void display() {
        System.out.print(name + " - 天气预报:");
        if (currentPressure > lastPressure) {
            System.out.println("天气正在变好");
        } else if (currentPressure < lastPressure) {
            System.out.println("天气正在变差");
        } else {
            System.out.println("天气稳定");
        }
    }
}

/**
 * 客户端
 */
public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        
        // 创建观察者并自动注册
        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay("手机App", weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay("气象站", weatherData);
        ForecastDisplay forecastDisplay = new ForecastDisplay("电视台", weatherData);
        
        System.out.println("\n=== 第一次天气更新 ===");
        weatherData.setMeasurements(25.5f, 65, 1013.2f);
        
        System.out.println("\n=== 移除电视台观察者 ===");
        weatherData.removeObserver(forecastDisplay);
        
        System.out.println("\n=== 第二次天气更新 ===");
        weatherData.setMeasurements(27.8f, 70, 1011.5f);
        
        System.out.println("\n=== 新增智能家居观察者 ===");
        SmartHomeDisplay smartHome = new SmartHomeDisplay("智能家居", weatherData);
        
        System.out.println("\n=== 第三次天气更新 ===");
        weatherData.setMeasurements(26.0f, 68, 1014.0f);
    }
}

输出

text

ini 复制代码
新增观察者,当前观察者数量:1
新增观察者,当前观察者数量:2
新增观察者,当前观察者数量:3

=== 第一次天气更新 ===
手机App - 当前天气:温度 25.5°C,湿度 65.0%
气象站 - 平均温度:25.5°C
电视台 - 天气预报:天气稳定

=== 移除电视台观察者 ===
移除观察者,当前观察者数量:2

=== 第二次天气更新 ===
手机App - 当前天气:温度 27.8°C,湿度 70.0%
气象站 - 平均温度:26.65°C

=== 新增智能家居观察者 ===
新增观察者,当前观察者数量:3

=== 第三次天气更新 ===
手机App - 当前天气:温度 26.0°C,湿度 68.0%
气象站 - 平均温度:26.433334°C
智能家居 - 检测到天气变化,关闭窗帘、调整空调

3.2 使用 Java 内置观察者模式

java

java 复制代码
import java.util.Observable;
import java.util.Observer;

/**
 * 具体主题(使用 Java 内置支持)
 */
public class WeatherDataV2 extends Observable {
    private float temperature;
    private float humidity;
    private float pressure;
    
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
    
    public void measurementsChanged() {
        // 标记状态已改变
        setChanged();
        // 通知所有观察者(推模式)
        notifyObservers(new Object[]{temperature, humidity, pressure});
        // 也可以使用拉模式:notifyObservers()
    }
    
    // 拉模式需要提供 getter
    public float getTemperature() { return temperature; }
    public float getHumidity() { return humidity; }
    public float getPressure() { return pressure; }
}

/**
 * 具体观察者(拉模式)
 */
public class CurrentConditionsDisplayV2 implements Observer {
    private Observable observable;
    private float temperature;
    private float humidity;
    
    public CurrentConditionsDisplayV2(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }
    
    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof WeatherDataV2) {
            WeatherDataV2 weatherData = (WeatherDataV2) o;
            // 拉模式:主动获取数据
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }
    
    public void display() {
        System.out.println("当前天气:" + temperature + "°C," + humidity + "%");
    }
}

四、实战场景

4.1 Spring 事件机制

Spring 基于观察者模式实现了事件发布-订阅机制。

java

typescript 复制代码
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * 自定义事件
 */
public class OrderEvent extends ApplicationEvent {
    private String orderId;
    private String status;
    
    public OrderEvent(Object source, String orderId, String status) {
        super(source);
        this.orderId = orderId;
        this.status = status;
    }
    
    public String getOrderId() { return orderId; }
    public String getStatus() { return status; }
}

/**
 * 事件发布者
 */
@Component
public class OrderService implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;
    
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
    
    public void createOrder(String orderId) {
        System.out.println("创建订单:" + orderId);
        // 发布订单创建事件
        publisher.publishEvent(new OrderEvent(this, orderId, "CREATED"));
    }
    
    public void payOrder(String orderId) {
        System.out.println("支付订单:" + orderId);
        publisher.publishEvent(new OrderEvent(this, orderId, "PAID"));
    }
}

/**
 * 事件监听器1:发送短信通知
 */
@Component
public class SmsListener implements ApplicationListener<OrderEvent> {
    @Override
    public void onApplicationEvent(OrderEvent event) {
        System.out.println("【短信】订单 " + event.getOrderId() + " 状态变更为:" + event.getStatus());
    }
}

/**
 * 事件监听器2:记录日志
 */
@Component
public class LogListener implements ApplicationListener<OrderEvent> {
    @Override
    public void onApplicationEvent(OrderEvent event) {
        System.out.println("【日志】订单 " + event.getOrderId() + " - " + event.getStatus() + " - " + new Date());
    }
}

/**
 * 事件监听器3:更新库存
 */
@Component
public class StockListener implements ApplicationListener<OrderEvent> {
    @Override
    public void onApplicationEvent(OrderEvent event) {
        if ("PAID".equals(event.getStatus())) {
            System.out.println("【库存】订单 " + event.getOrderId() + " 支付成功,扣减库存");
        }
    }
}

// 使用
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;
    
    @PostMapping("/order")
    public String createOrder() {
        orderService.createOrder("ORD_001");
        orderService.payOrder("ORD_001");
        return "success";
    }
}

4.2 消息队列的发布-订阅

java

typescript 复制代码
/**
 * 消息发布者
 */
public class MessagePublisher {
    private List<MessageSubscriber> subscribers = new ArrayList<>();
    
    public void subscribe(MessageSubscriber subscriber) {
        subscribers.add(subscriber);
    }
    
    public void unsubscribe(MessageSubscriber subscriber) {
        subscribers.remove(subscriber);
    }
    
    public void publish(String topic, String message) {
        System.out.println("发布消息到 [" + topic + "]:" + message);
        for (MessageSubscriber subscriber : subscribers) {
            if (subscriber.getTopic().equals(topic)) {
                subscriber.onMessage(message);
            }
        }
    }
}

/**
 * 订阅者接口
 */
public interface MessageSubscriber {
    String getTopic();
    void onMessage(String message);
}

/**
 * 邮件订阅者
 */
public class EmailSubscriber implements MessageSubscriber {
    private String email;
    private String topic;
    
    public EmailSubscriber(String email, String topic) {
        this.email = email;
        this.topic = topic;
    }
    
    @Override
    public String getTopic() { return topic; }
    
    @Override
    public void onMessage(String message) {
        System.out.println("发送邮件到 " + email + ":【" + topic + "】" + message);
    }
}

/**
 * Webhook订阅者
 */
public class WebhookSubscriber implements MessageSubscriber {
    private String url;
    private String topic;
    
    public WebhookSubscriber(String url, String topic) {
        this.url = url;
        this.topic = topic;
    }
    
    @Override
    public String getTopic() { return topic; }
    
    @Override
    public void onMessage(String message) {
        System.out.println("调用Webhook " + url + ":【" + topic + "】" + message);
    }
}

// 使用
MessagePublisher publisher = new MessagePublisher();
publisher.subscribe(new EmailSubscriber("user@example.com", "订单"));
publisher.subscribe(new WebhookSubscriber("https://api.example.com/order", "订单"));
publisher.subscribe(new EmailSubscriber("admin@example.com", "告警"));

publisher.publish("订单", "新订单 #12345 已创建");
publisher.publish("告警", "服务器 CPU 使用率超过 90%");

4.3 股票行情系统

java

csharp 复制代码
/**
 * 股票主题
 */
public class StockSubject {
    private Map<String, List<StockObserver>> observers = new HashMap<>();
    
    public void registerObserver(String stockCode, StockObserver observer) {
        observers.computeIfAbsent(stockCode, k -> new ArrayList<>()).add(observer);
    }
    
    public void removeObserver(String stockCode, StockObserver observer) {
        List<StockObserver> list = observers.get(stockCode);
        if (list != null) {
            list.remove(observer);
        }
    }
    
    public void notifyObservers(String stockCode, double price) {
        List<StockObserver> list = observers.get(stockCode);
        if (list != null) {
            for (StockObserver observer : list) {
                observer.update(stockCode, price);
            }
        }
    }
    
    public void setPrice(String stockCode, double price) {
        System.out.println("\n股票 " + stockCode + " 价格变为:" + price);
        notifyObservers(stockCode, price);
    }
}

/**
 * 观察者接口
 */
public interface StockObserver {
    void update(String stockCode, double price);
}

/**
 * 投资者观察者
 */
public class Investor implements StockObserver {
    private String name;
    private double buyThreshold;
    private double sellThreshold;
    
    public Investor(String name, double buyThreshold, double sellThreshold) {
        this.name = name;
        this.buyThreshold = buyThreshold;
        this.sellThreshold = sellThreshold;
    }
    
    @Override
    public void update(String stockCode, double price) {
        if (price <= buyThreshold) {
            System.out.println(name + ":价格跌到 " + price + ",建议买入 " + stockCode);
        } else if (price >= sellThreshold) {
            System.out.println(name + ":价格涨到 " + price + ",建议卖出 " + stockCode);
        } else {
            System.out.println(name + ":" + stockCode + " 当前价格 " + price + ",观望中");
        }
    }
}

// 使用
StockSubject stockMarket = new StockSubject();

Investor alice = new Investor("张三", 95, 110);
Investor bob = new Investor("李四", 98, 108);
Investor carol = new Investor("王五", 90, 120);

stockMarket.registerObserver("000001", alice);
stockMarket.registerObserver("000001", bob);
stockMarket.registerObserver("000002", carol);

stockMarket.setPrice("000001", 100);
stockMarket.setPrice("000001", 96);
stockMarket.setPrice("000001", 112);
stockMarket.setPrice("000002", 95);

五、推模式 vs 拉模式

对比 推模式 拉模式
通知方式 主题把所有数据推给观察者 观察者主动从主题拉取数据
参数传递 update(数据1, 数据2, 数据3) update() 或 空参数
优点 观察者不需要知道主题细节 观察者按需获取数据
缺点 数据量大时效率低;数据变更要改接口 需要额外调用 getter
适用场景 数据量小且固定 数据量大,观察者需要部分数据

java

java 复制代码
// 推模式
public interface PushObserver {
    void update(float temp, float humidity, float pressure);
}

// 拉模式
public interface PullObserver {
    void update(WeatherData subject);
}

public class PullDisplay implements PullObserver {
    @Override
    public void update(WeatherData subject) {
        // 按需获取数据
        float temp = subject.getTemperature();
        float humidity = subject.getHumidity();
        System.out.println("温度:" + temp + ",湿度:" + humidity);
    }
}

六、观察者模式 vs 其他模式

对比 观察者模式 发布-订阅模式 中介者模式 责任链模式
关系 一对多 多对多 多对多 链式
耦合度 极低(通过消息代理)
是否知道对方 观察者知道主题 发布者和订阅者不知道对方 同事知道中介者 每个节点知道下一节点
典型例子 GUI事件监听 RabbitMQ、Kafka MVC控制器 过滤器链

七、常见面试题

Q1:观察者模式的优缺点?

优点

优点 说明
松耦合 主题和观察者之间只依赖接口
开闭原则 新增观察者不需要修改主题
支持广播通信 一个变化通知所有观察者
可动态注册 运行时可以添加/移除观察者

缺点

缺点 说明
顺序不可控 观察者通知顺序不确定
性能问题 观察者过多时通知耗时
循环依赖 观察者和主题可能循环调用
内存泄漏 观察者忘记注销会导致内存泄漏

Q2:JDK 内置 Observable 为什么被废弃?

  • Observable 是类不是接口,限制了灵活性
  • 状态管理不完善(setChanged 需要手动调用)
  • 通知顺序不可控
  • 不支持泛型
  • 推荐使用 PropertyChangeListener 或第三方库

Q3:观察者模式和发布-订阅模式的区别?

区别 观察者模式 发布-订阅模式
通信方式 主题直接通知观察者 通过消息代理(Broker)
耦合度 松耦合(观察者知道主题) 完全解耦(互不知道)
同步/异步 通常同步 通常异步
典型实现 事件监听 消息队列(RabbitMQ、Kafka)

Q4:如何避免观察者模式的内存泄漏?

java

typescript 复制代码
// 解决方案1:使用弱引用
public class WeakObserverCache {
    private Map<String, WeakReference<Observer>> observers = new HashMap<>();
    
    public void register(String key, Observer observer) {
        observers.put(key, new WeakReference<>(observer));
    }
    
    public void notifyAll() {
        Iterator<Map.Entry<String, WeakReference<Observer>>> it = observers.entrySet().iterator();
        while (it.hasNext()) {
            Observer observer = it.next().getValue().get();
            if (observer == null) {
                it.remove();  // 自动清理
            } else {
                observer.update();
            }
        }
    }
}

// 解决方案2:在观察者中正确注销
public class MyObserver implements Observer {
    private Subject subject;
    
    public MyObserver(Subject subject) {
        this.subject = subject;
        subject.registerObserver(this);
    }
    
    // 提供清理方法
    public void dispose() {
        subject.removeObserver(this);
    }
}

八、总结与速记卡

核心要点

text

复制代码
观察者模式 = 主题维护观察者列表 + 状态变化时通知所有观察者

代码模板

java

csharp 复制代码
// 主题接口
public interface Subject {
    void attach(Observer o);
    void detach(Observer o);
    void notify();
}

// 观察者接口
public interface Observer {
    void update();
}

// 具体主题
public class ConcreteSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private int state;
    
    public void setState(int state) {
        this.state = state;
        notify();
    }
    
    public void attach(Observer o) { observers.add(o); }
    public void detach(Observer o) { observers.remove(o); }
    public void notify() {
        for (Observer o : observers) {
            o.update();
        }
    }
}

一句话记忆

场景 方案
一个变化影响多个对象 观察者模式 ✅
需要解耦事件发布和响应 观察者模式 ✅
微信公众号订阅 观察者模式 ✅
GUI 按钮点击事件 观察者模式 ✅
分布式系统异步通信 发布-订阅模式 ✅
相关推荐
追烽少年x10 小时前
STL中的设计模式(二)
c++·设计模式
悟051510 小时前
设计模式-模板模式
设计模式
BLSxiaopanlaile11 小时前
有关创建型的几个设计模式总结
设计模式
蜡笔小马11 小时前
14.C++设计模式-状态模式
c++·设计模式·状态模式
加油201912 小时前
嵌入式软件技术栈和学习路线详解
linux·arm开发·数据结构·mqtt·设计模式·嵌入式
likerhood12 小时前
设计模式 · 代理模式(Proxy Pattern)java
java·设计模式·代理模式
刀法如飞1 天前
Palantir Ontology 存储结构与读写机制原理深入剖析
大数据·设计模式·系统架构
KobeSacre1 天前
设计模式——七大设计原则
设计模式
倒流时光三十年1 天前
设计模式 之 责任链模式
设计模式·责任链模式