10、观察者模式实战:气象站数据更新系统、股票价格变动通知系统

观察者模式,说白了就是「发布-订阅」那一套。一个对象状态变了,其他依赖它的对象能自动收到通知。我最早接触这个模式,是在做一个物联网气象站项目的时候。当时被各种传感器数据刷新搞得焦头烂额,后来用观察者模式一重构,整个世界都清净了。

10.1 观察者模式的核心思想

先别急着看代码,咱们把概念理清楚。观察者模式里有两个关键角色:

  • Subject(主题):也叫被观察者,它维护着一组观察者列表,状态变化时负责通知大家。
  • Observer(观察者):订阅主题的对象,主题一有风吹草动,它就能收到更新。

你想想看,这像不像你关注了一个公众号?公众号(主题)发文章,所有粉丝(观察者)都能收到推送。公众号不需要知道每个粉丝是谁,粉丝也不需要天天去刷公众号有没有更新——这就是解耦。

核心原则:主题和观察者之间是松耦合的。主题只依赖观察者接口,不依赖具体实现。这样新增观察者时,主题代码完全不用改。

10.2 气象站数据更新系统实战

我记得当年做气象站项目时,最头疼的就是数据展示模块。温度、湿度、气压三个传感器,每个都要刷新到三个不同的显示屏上。一开始我写了个大循环,每次数据更新就挨个调用显示方法。后来需求一变,要加第四个显示屏……我改代码改到凌晨两点。

用观察者模式就优雅多了。来看代码:

// 观察者接口
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);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @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();
    }

    private void measurementsChanged() {
        notifyObservers();
    }
}

// 当前状况显示屏(观察者)
public class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    public void display() {
        System.out.println("当前温度: " + temperature + "°C, 湿度: " + humidity + "%");
    }
}

// 统计显示屏(观察者)
public class StatisticsDisplay implements Observer {
    private float maxTemp = Float.MIN_VALUE;
    private float minTemp = Float.MAX_VALUE;
    private float tempSum = 0.0f;
    private int numReadings = 0;

    @Override
    public void update(float temperature, float humidity, float pressure) {
        tempSum += temperature;
        numReadings++;
        if (temperature > maxTemp) maxTemp = temperature;
        if (temperature < minTemp) minTemp = temperature;
        display();
    }

    public void display() {
        System.out.println("平均温度: " + (tempSum / numReadings) + "°C, 最高: " + maxTemp + "°C, 最低: " + minTemp + "°C");
    }
}

我的习惯:在实际项目中,我通常会把观察者接口定义成泛型,这样 update 方法可以传递任意类型的参数,而不是固定三个 float。比如 void update(WeatherData data),这样扩展性更好。

10.3 股票价格变动通知系统

股票系统跟气象站其实是一个套路。只不过气象站通知的是天气数据,股票系统通知的是股价变动。我参与过一个金融数据平台,里面就有类似的设计。

来看股票系统的实现:

// 股票数据(主题)
public class StockData implements Subject {
    private List<Observer> observers;
    private String stockSymbol;
    private float price;
    private float changePercent;

    public StockData(String stockSymbol) {
        this.stockSymbol = stockSymbol;
        this.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() {
        for (Observer observer : observers) {
            observer.update(stockSymbol, price, changePercent);
        }
    }

    public void setPrice(float newPrice) {
        float oldPrice = this.price;
        this.price = newPrice;
        this.changePercent = ((newPrice - oldPrice) / oldPrice) * 100;
        notifyObservers();
    }
}

// 股票交易员(观察者)
public class TraderDisplay implements Observer {
    private String name;

    public TraderDisplay(String name) {
        this.name = name;
    }

    @Override
    public void update(String symbol, float price, float changePercent) {
        System.out.println(name + " 收到通知: " + symbol + " 当前价格 $" + price + " 涨跌幅 " + String.format("%.2f", changePercent) + "%");
        if (changePercent > 5.0f) {
            System.out.println("  警告: 涨幅超过5%,建议关注!");
        }
    }
}

// 移动端推送服务(观察者)
public class MobilePushService implements Observer {
    @Override
    public void update(String symbol, float price, float changePercent) {
        // 实际项目中这里会调用推送API
        System.out.println("[推送] " + symbol + " 价格变动至 $" + price);
    }
}

我曾经踩过的坑:在股票系统中,如果观察者数量很多(比如上千个客户端),同步通知会导致主题线程阻塞。我曾经遇到过因为一个观察者的 update 方法抛异常,导致整个通知链中断的问题。解决方案有两个:一是用异步通知(线程池),二是在通知循环里加 try-catch,保证一个观察者出问题不影响其他人。

10.4 观察者模式的核心流程图

下面这张图展示了观察者模式的完整数据流。我习惯用这种图来跟团队成员沟通设计思路,一目了然。

观察者模式数据流图 Subject(主题) WeatherData / StockData Observer 列表 ArrayList<Observer> 维护 数据源 传感器 / 交易所API setMeasurements() 观察者1 CurrentConditionsDisplay 观察者2 StatisticsDisplay 观察者3 MobilePushService notifyObservers() 遍历列表,调用 update() 数据源变化 → 主题更新数据 → 通知所有观察者 → 观察者各自处理 关键点:主题不知道观察者的具体类型,只依赖 Observer 接口 新增观察者时,主题代码完全不需要修改 —— 开闭原则

10.5 观察者模式的优缺点

用了几次观察者模式之后,我总结了一些经验。好东西也有它的适用场景,别盲目用。

维度 优点 缺点
耦合度 主题和观察者松耦合,主题只依赖接口 观察者之间可能产生隐式依赖,调试困难
扩展性 新增观察者无需修改主题代码 观察者过多时,通知顺序不可控
性能 数据变化时即时通知,响应快 同步通知可能阻塞主题线程
维护性 代码结构清晰,职责分明 观察者被移除后可能造成内存泄漏

我的建议:如果你发现一个对象的状态变化需要触发多个其他对象的更新,而且这些对象可能随时增减,那就果断用观察者模式。但如果你只有一两个固定的依赖对象,直接调用方法反而更简单,别为了用模式而用模式。

10.6 避坑指南

最后分享几个我踩过的坑,希望能帮你少走弯路。

  • 内存泄漏问题:观察者注册后忘记移除,会导致主题持有观察者的引用,垃圾回收无法回收。我曾经在一个长连接服务里犯过这个错,内存一路飙升到 OOM。解决方案是在观察者的生命周期结束时,主动调用 removeObserver()
  • 通知顺序依赖:有些同学会依赖观察者的通知顺序来写业务逻辑。千万别这么干!不同语言、不同容器的遍历顺序可能不同。如果必须有序,可以考虑用优先级队列。
  • 循环通知:观察者的 update 方法里又去修改主题数据,导致无限循环。我见过一个案例,A 观察者更新了 B 的数据,B 又反过来更新 A……死循环了。解决方案是在通知时加一个状态锁,或者用事件队列做异步处理。
  • 异常处理:前面提过,一个观察者抛异常会导致整个通知链中断。一定要在通知循环里加 try-catch,保证健壮性。

观察者模式是我个人非常喜欢的一个设计模式。它把「一对多」的依赖关系管理得明明白白。气象站和股票系统只是两个经典案例,你可以在事件驱动架构、消息队列、GUI 事件监听等场景中看到它的身影。嗯,用好它,你的代码会干净很多。

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