1. 观察者模式:状态变化的广播机制
想象一下你正在开发一个气象站应用。当温度传感器检测到数据变化时,需要同时更新手机App、大屏显示器和数据库记录——这种一对多的通知场景正是观察者模式的用武之地。观察者模式定义了对象间的一种一对多的依赖关系,当一个对象(被观察者)的状态发生改变时,所有依赖它的对象(观察者)都会自动收到通知并更新。
这个模式的核心价值在于解耦。传统做法可能会让温度传感器直接调用各个显示模块的更新方法,导致传感器代码与显示逻辑紧耦合。而观察者模式通过抽象接口将两者解耦,让传感器只需维护观察者列表并发送通知,完全不用关心谁接收、如何处理这些通知。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模式结构与核心组件
2.1 UML类图解析
观察者模式的标准实现包含两个核心接口和若干具体实现类:
code复制Subject (被观察者接口)
|—— attach(Observer o)
|—— detach(Observer o)
|—— notify()
ConcreteSubject (具体被观察者)
|—— _observers: List<Observer>
|—— _state: Object
|—— getState()
|—— setState()
Observer (观察者接口)
|—— update()
ConcreteObserver (具体观察者)
|—— _subject: Subject
|—— update()
2.2 关键组件职责说明
- Subject:维护观察者列表,提供添加/删除观察者的方法,定义通知机制
- ConcreteSubject:存储具体状态,状态变更时调用notify()触发通知
- Observer:定义更新接口,用于接收Subject的状态变更通知
- ConcreteObserver:实现具体更新逻辑,通常持有Subject引用以获取最新状态
提示:在Java中可以使用java.util.Observable和Observer接口,但自Java 9后已被标记为@Deprecated,建议自行实现这套机制以获得更灵活的控制。
3. 实战:气象站监测系统实现
3.1 基础版本实现
我们以气象站为例展示一个完整实现。首先定义被观察者接口:
java复制public interface WeatherStation {
void registerObserver(DisplayDevice observer);
void removeObserver(DisplayDevice observer);
void notifyObservers();
}
具体气象站实现温度监测逻辑:
java复制public class ConcreteWeatherStation implements WeatherStation {
private List<DisplayDevice> devices = new ArrayList<>();
private float temperature;
@Override
public void registerObserver(DisplayDevice observer) {
devices.add(observer);
}
@Override
public void notifyObservers() {
for (DisplayDevice device : devices) {
device.update(temperature);
}
}
public void setTemperature(float newTemp) {
this.temperature = newTemp;
notifyObservers(); // 关键:状态变化时自动通知
}
}
显示设备作为观察者:
java复制public interface DisplayDevice {
void update(float temperature);
}
public class MobileDisplay implements DisplayDevice {
@Override
public void update(float temp) {
System.out.println("手机端更新温度:" + temp + "℃");
// 实际项目这里会触发UI重绘
}
}
3.2 推模型 vs 拉模型
观察者模式有两种通知方式:
- 推模型:Subject将变更数据作为参数直接推送给Observer(如上例)
- 拉模型:Subject只通知变更,Observer主动调用Subject方法获取数据
推模型效率更高但不够灵活,拉模型更通用但可能引发多次调用。实际开发中可根据场景选择:
java复制// 拉模型实现示例
public void notifyObservers() {
for (Observer o : observers) {
o.update(this); // 将Subject自身传给Observer
}
}
public class ObserverImpl implements Observer {
public void update(Subject subject) {
ConcreteSubject s = (ConcreteSubject)subject;
System.out.println("新状态:" + s.getState());
}
}
4. 生产环境中的进阶实践
4.1 异步通知优化
在高频状态变更场景(如股票行情),同步通知可能导致性能瓶颈。我们可以引入线程池实现异步通知:
java复制private ExecutorService executor = Executors.newCachedThreadPool();
public void notifyObservers() {
for (DisplayDevice device : devices) {
executor.submit(() -> device.update(temperature));
}
}
注意:异步处理需要考虑线程安全和通知顺序问题。建议:
- 使用CopyOnWriteArrayList避免并发修改异常
- 对顺序敏感的场景使用单线程Executor
4.2 防止观察者内存泄漏
观察者长期注册但不注销会导致内存泄漏。解决方案包括:
- 使用WeakReference存储观察者
- 提供明确的注销机制
- 生命周期管理(如Android的LifecycleObserver)
java复制// 弱引用实现示例
private List<WeakReference<DisplayDevice>> weakDevices = new ArrayList<>();
public void registerObserver(DisplayDevice observer) {
weakDevices.add(new WeakReference<>(observer));
}
public void notifyObservers() {
Iterator<WeakReference<DisplayDevice>> it = weakDevices.iterator();
while (it.hasNext()) {
DisplayDevice device = it.next().get();
if (device != null) {
device.update(temperature);
} else {
it.remove(); // 自动清理失效引用
}
}
}
5. 模式变体与行业应用
5.1 事件总线实现
观察者模式的扩展应用是事件总线(EventBus),它通常:
- 使用字符串或类型作为事件标识
- 支持全局事件分发
- 提供注解驱动的事件处理
java复制// 简化版事件总线核心逻辑
public class EventBus {
private Map<Class<?>, List<Consumer<?>>> handlers = new HashMap<>();
public <T> void subscribe(Class<T> eventType, Consumer<T> handler) {
handlers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
}
public <T> void publish(T event) {
List<Consumer<?>> consumers = handlers.get(event.getClass());
if (consumers != null) {
consumers.forEach(c -> ((Consumer<T>)c).accept(event));
}
}
}
5.2 典型应用场景
- GUI事件处理:按钮点击、键盘输入等事件监听
- 分布式系统:配置中心变更通知多服务
- 游戏开发:角色状态变化触发UI/音效更新
- 物联网:传感器数据变化触发多端响应
- 金融系统:价格变动通知多个交易策略
6. 与其他模式的对比与协作
6.1 观察者 vs 发布订阅
两者常被混淆,关键区别在于:
- 观察者:直接通信,观察者知道被观察者
- 发布订阅:通过中间件通信,发布订阅双方互不知晓
mermaid复制// 禁止使用mermaid图表,改用文字描述:
观察者模式是Subject直接维护Observer列表并调用其方法;而发布订阅模式中,Publisher和Subscriber通过Broker间接通信,Publisher将事件发送到特定通道,Broker负责将事件路由给订阅该通道的Subscriber。
6.2 与中介者模式协作
当多个Subject和Observer交互时,可以引入Mediator来集中管理交互逻辑,避免网状依赖:
java复制public class NotificationMediator {
private Map<Class<?>, List<Observer>> mappings = new ConcurrentHashMap<>();
public void register(Class<?> eventType, Observer observer) {
mappings.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
.add(observer);
}
public void notify(Object event) {
List<Observer> observers = mappings.get(event.getClass());
if (observers != null) {
observers.forEach(o -> o.update(event));
}
}
}
7. 性能优化与问题排查
7.1 高频更新场景优化
当遇到状态高频变化(如鼠标移动事件)时,可采用以下策略:
- 节流通知:使用时间戳控制最小通知间隔
- 批量更新:累积多次变化后一次性通知
- 差异检测:仅当数据实际变化时才通知
java复制// 节流实现示例
private long lastNotifyTime;
private static final long NOTIFY_INTERVAL = 100; // 100ms
public void setTemperature(float newTemp) {
this.temperature = newTemp;
long now = System.currentTimeMillis();
if (now - lastNotifyTime > NOTIFY_INTERVAL) {
notifyObservers();
lastNotifyTime = now;
}
}
7.2 常见问题排查
- 通知丢失:检查观察者注册时机,确保在状态变化前完成注册
- 更新顺序问题:需要特定顺序时,可使用PriorityQueue存储观察者
- 循环通知:避免观察者修改状态触发二次通知
- 性能瓶颈:使用Profiler工具分析通知耗时
我在实际项目中曾遇到一个隐蔽bug:当某个观察者的update方法抛出异常时,后续观察者无法收到通知。解决方案是给每个通知添加try-catch块:
java复制public void notifyObservers() {
for (DisplayDevice device : devices) {
try {
device.update(temperature);
} catch (Exception e) {
logger.error("Observer处理失败", e);
}
}
}
8. 现代语言中的新特性应用
8.1 Java中的响应式流
Java 9引入的Flow API提供了标准的发布-订阅接口:
java复制public class TemperaturePublisher implements Publisher<Float> {
private final SubmissionPublisher<Float> publisher = new SubmissionPublisher<>();
public void subscribe(Subscriber<? super Float> subscriber) {
publisher.subscribe(subscriber);
}
public void newTemperature(float temp) {
publisher.submit(temp);
}
}
// 使用示例
TempPublisher pub = new TempPublisher();
pub.subscribe(new Subscriber<>() {
public void onNext(Float temp) {
System.out.println("收到温度:" + temp);
}
// 其他方法省略...
});
8.2 C#中的事件机制
C#原生支持事件语法糖,本质是观察者模式的语法封装:
csharp复制public class WeatherStation {
public event Action<float> TemperatureChanged;
private float _temperature;
public float Temperature {
get => _temperature;
set {
_temperature = value;
TemperatureChanged?.Invoke(value); // 触发事件
}
}
}
// 订阅事件
station.TemperatureChanged += temp => Console.WriteLine($"温度更新:{temp}");
8.3 JavaScript中的Proxy应用
利用ES6 Proxy可以实现自动化的观察者通知:
javascript复制const weather = {
_temperature: 0,
_observers: new Set(),
subscribe(observer) {
this._observers.add(observer)
},
get temperature() {
return this._temperature
},
set temperature(value) {
this._temperature = value
this._observers.forEach(o => o(value))
}
}
// 创建代理
const weatherProxy = new Proxy(weather, {
set(target, prop, value) {
target[prop] = value
if (prop === '_temperature') {
target._observers.forEach(o => o(value))
}
return true
}
})
9. 测试策略与Mock实践
9.1 单元测试要点
测试观察者模式时需要验证:
- 状态变化确实触发了通知
- 所有注册的观察者都收到了通知
- 通知携带了正确的数据
- 取消注册后不再收到通知
使用Mock框架的示例(Mockito):
java复制@Test
public void testTemperatureNotification() {
// 准备
WeatherStation station = new ConcreteWeatherStation();
DisplayDevice mockDisplay = mock(DisplayDevice.class);
station.registerObserver(mockDisplay);
// 执行
station.setTemperature(25.5f);
// 验证
verify(mockDisplay).update(25.5f);
verifyNoMoreInteractions(mockDisplay);
}
@Test
public void testUnregisterObserver() {
WeatherStation station = new ConcreteWeatherStation();
DisplayDevice mockDisplay = mock(DisplayDevice.class);
station.registerObserver(mockDisplay);
station.removeObserver(mockDisplay);
station.setTemperature(30f);
verify(mockDisplay, never()).update(anyFloat());
}
9.2 集成测试场景
模拟真实环境中的复杂场景:
- 多个观察者同时注册/注销
- 高频状态变化下的通知稳定性
- 观察者处理超时或失败的情况
- 跨线程通知的正确性
java复制@Test
public void testConcurrentAccess() throws InterruptedException {
final WeatherStation station = new ConcreteWeatherStation();
final int threadCount = 10;
final CountDownLatch latch = new CountDownLatch(threadCount);
// 创建多个线程同时操作
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
DisplayDevice dev = mock(DisplayDevice.class);
station.registerObserver(dev);
station.setTemperature(ThreadLocalRandom.current().nextFloat());
station.removeObserver(dev);
latch.countDown();
}).start();
}
assertTrue(latch.await(5, TimeUnit.SECONDS));
// 验证没有并发异常发生
}
10. 反模式与滥用警示
虽然观察者模式功能强大,但不当使用会导致系统难以维护:
10.1 常见反模式
- 过度通知:高频无意义的状态变化通知
- 隐式耦合:观察者对被观察者实现细节的假设
- 链式反应:观察者修改状态触发新的通知,形成无限循环
- 性能黑洞:同步阻塞式通知拖慢主流程
10.2 适用场景判断
适合使用观察者模式的情况:
✅ 一个对象状态变化需要通知其他对象
✅ 不希望被观察者和观察者紧密耦合
✅ 观察者的数量和身份可能动态变化
不适合的情况:
❌ 通知链路过长或形成环状依赖
❌ 对性能有极端要求的实时系统
❌ 观察者需要知道被观察者的详细实现
我在重构一个电商促销系统时,曾将原本分散在各处的价格更新逻辑改用观察者模式统一管理。初期效果很好,但随着业务复杂化,出现了通知风暴问题——某个商品属性的变更会触发数十个观察者处理,导致接口响应变慢。最终解决方案是引入异步队列和批量处理机制,同时严格区分核心属性变更和衍生属性更新。
