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 观察者模式的核心流程图
下面这张图展示了观察者模式的完整数据流。我习惯用这种图来跟团队成员沟通设计思路,一目了然。
10.5 观察者模式的优缺点
用了几次观察者模式之后,我总结了一些经验。好东西也有它的适用场景,别盲目用。
| 维度 | 优点 | 缺点 |
|---|---|---|
| 耦合度 | 主题和观察者松耦合,主题只依赖接口 | 观察者之间可能产生隐式依赖,调试困难 |
| 扩展性 | 新增观察者无需修改主题代码 | 观察者过多时,通知顺序不可控 |
| 性能 | 数据变化时即时通知,响应快 | 同步通知可能阻塞主题线程 |
| 维护性 | 代码结构清晰,职责分明 | 观察者被移除后可能造成内存泄漏 |
我的建议:如果你发现一个对象的状态变化需要触发多个其他对象的更新,而且这些对象可能随时增减,那就果断用观察者模式。但如果你只有一两个固定的依赖对象,直接调用方法反而更简单,别为了用模式而用模式。
10.6 避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
- 内存泄漏问题:观察者注册后忘记移除,会导致主题持有观察者的引用,垃圾回收无法回收。我曾经在一个长连接服务里犯过这个错,内存一路飙升到 OOM。解决方案是在观察者的生命周期结束时,主动调用
removeObserver()。 - 通知顺序依赖:有些同学会依赖观察者的通知顺序来写业务逻辑。千万别这么干!不同语言、不同容器的遍历顺序可能不同。如果必须有序,可以考虑用优先级队列。
- 循环通知:观察者的 update 方法里又去修改主题数据,导致无限循环。我见过一个案例,A 观察者更新了 B 的数据,B 又反过来更新 A……死循环了。解决方案是在通知时加一个状态锁,或者用事件队列做异步处理。
- 异常处理:前面提过,一个观察者抛异常会导致整个通知链中断。一定要在通知循环里加 try-catch,保证健壮性。
观察者模式是我个人非常喜欢的一个设计模式。它把「一对多」的依赖关系管理得明明白白。气象站和股票系统只是两个经典案例,你可以在事件驱动架构、消息队列、GUI 事件监听等场景中看到它的身影。嗯,用好它,你的代码会干净很多。