18. 观察者模式:定义、结构、实现(推模型、拉模型)、优缺点、应用场景

观察者模式,说白了就是「发布-订阅」机制。一个对象状态变了,其他依赖它的对象能自动收到通知。我刚开始接触设计模式时,觉得这玩意儿不就是事件监听吗?后来在项目中踩过坑才明白——它远不止这么简单。

18.1 定义与核心思想

观察者模式定义了一种一对多的依赖关系。当被观察者(Subject)状态发生变化时,所有依赖它的观察者(Observer)都会收到通知并自动更新。

嗯,这里要注意:观察者模式强调的是「松耦合」。被观察者不需要知道观察者的具体实现,只需要知道它们实现了同一个接口。我在一个电商项目中就用到了这个特性——订单状态变了,库存系统、物流系统、通知系统各自做自己的事,互不干扰。

18.2 结构组成

观察者模式的结构其实很清晰,就四个角色:

角色 职责 我见过的坑
Subject(目标) 维护观察者列表,提供注册/删除方法 很多人忘记考虑线程安全问题
ConcreteSubject(具体目标) 状态变化时通知所有观察者 通知顺序依赖导致的问题
Observer(观察者) 定义更新接口 接口设计太粗或太细
ConcreteObserver(具体观察者) 实现更新逻辑 更新逻辑里又触发了新的通知

核心要点:被观察者只依赖观察者接口,不依赖具体实现。这是依赖倒置原则的典型应用。

18.3 推模型 vs 拉模型

这是观察者模式里最容易让人纠结的地方。我当年也困惑过——到底该把数据推给观察者,还是让观察者自己来拉?

推模型(Push Model)

被观察者主动把详细信息推给观察者。说白了就是「我给你,你接着」。

// 推模型:被观察者把数据作为参数传过去
public interface Observer {
    void update(String temperature, String humidity, String pressure);
}

public class WeatherStation implements Subject {
    private String temperature;
    private String humidity;
    private String pressure;
    
    public void notifyObservers() {
        for (Observer observer : observers) {
            // 直接把三个数据都推过去
            observer.update(temperature, humidity, pressure);
        }
    }
}

优点:观察者拿到数据就能用,不用再查询。响应快。

缺点:如果数据项很多,接口参数会爆炸。而且有些观察者可能只需要其中一部分数据,造成了浪费。

我的经验:推模型适合数据量小、所有观察者都需要全部数据的场景。比如股票行情推送,每个客户端都需要最新的价格和成交量。

拉模型(Pull Model)

被观察者只通知「我变了」,具体数据由观察者自己来取。说白了就是「我告诉你变了,你自己来看」。

// 拉模型:只传递被观察者自身引用
public interface Observer {
    void update(Subject subject);
}

public class WeatherStation implements Subject {
    private String temperature;
    private String humidity;
    private String pressure;
    
    public void notifyObservers() {
        for (Observer observer : observers) {
            // 只传自己,观察者按需获取
            observer.update(this);
        }
    }
    
    // 观察者通过 getter 按需获取
    public String getTemperature() { return temperature; }
    public String getHumidity() { return humidity; }
    public String getPressure() { return pressure; }
}

public class PhoneDisplay implements Observer {
    public void update(Subject subject) {
        WeatherStation ws = (WeatherStation) subject;
        // 我只关心温度,只拉取温度
        this.temperature = ws.getTemperature();
    }
}

优点:接口稳定,观察者按需获取,灵活度高。

缺点:观察者需要知道被观察者的具体类型(需要向下转型),耦合度略高。

避坑指南:我曾经在一个项目中用了拉模型,结果观察者每次更新都去数据库查数据,导致数据库压力暴增。后来加了缓存才解决。拉模型要注意——别让观察者做太重的查询操作。

18.4 完整实现示例

我习惯用推模型实现一个简单的天气站。你想想看,这样更直观:

// 观察者接口
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<>();
    }
    
    public void registerObserver(Observer o) {
        observers.add(o);
    }
    
    public void removeObserver(Observer o) {
        observers.remove(o);
    }
    
    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();
    }
    
    private void measurementsChanged() {
        notifyObservers();
    }
}

// 具体观察者
public class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;
    
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    public void display() {
        System.out.println("当前温度: " + temperature + "°C");
        System.out.println("当前湿度: " + humidity + "%");
    }
}

18.5 优缺点分析

优点 缺点
松耦合:被观察者和观察者之间只依赖抽象接口 通知顺序不可控:多个观察者时,更新顺序不确定
支持广播通信:一对多通知,效率高 可能引起循环依赖:观察者更新又触发被观察者变化
符合开闭原则:新增观察者无需修改被观察者 性能问题:观察者太多时通知开销大
运行时动态建立订阅关系 内存泄漏风险:忘记注销观察者会导致对象无法回收

我曾经踩过的坑:在一个Android项目中,Activity作为观察者注册到全局的EventBus,但Activity销毁时忘记注销。结果Activity对象一直被持有,导致内存泄漏。后来我强制要求所有观察者必须在onDestroy中注销,才解决了这个问题。

18.6 应用场景

观察者模式在实际项目中太常见了。我总结了几类典型场景:

  • 事件驱动系统:GUI框架中的按钮点击事件、鼠标移动事件。Java的Swing、Android的OnClickListener都是观察者模式。
  • 消息队列/发布订阅:Kafka、RabbitMQ本质上就是观察者模式的分布式版本。生产者发布消息,消费者订阅消息。
  • 数据同步:一个数据源变化,多个视图需要同步更新。比如Excel表格中,修改一个单元格,所有引用该单元格的公式自动重算。
  • 状态监控:服务器状态变化时,监控系统、告警系统、日志系统各自响应。
  • 响应式编程:RxJava、Reactor等框架的核心思想就是观察者模式。

我的建议:如果你发现代码中需要频繁轮询来获取状态变化,或者多个模块需要响应同一个事件,那就该考虑观察者模式了。但别滥用——如果只有一两个观察者,直接用回调函数更简单。

18.7 知识体系图

观察者模式知识体系 一对多依赖关系 Subject(目标) Observer(观察者) ConcreteSubject 推模型(Push) 拉模型(Pull) 优点:松耦合、广播通信、开闭原则、动态订阅 事件驱动系统 消息队列/发布订阅 数据同步 状态监控

18.8 总结

观察者模式是我用得最多的设计模式之一。它解决的核心问题就是「当A变了,B、C、D怎么知道?」。推模型和拉模型各有优劣,我个人的习惯是:数据量小、所有观察者都需要时用推模型;数据量大、观察者各取所需时用拉模型。

最后提醒一句:观察者模式虽好,但别滥用。如果只是简单的回调,用匿名内部类或Lambda表达式就够了。模式是工具,不是目的。


公众号:蓝海资料掘金营,微信deep3321