Observer Design Pattern

GoF 给出的定义: Define a one to many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

当一个对象状态变化时(被观察者),多个依赖的对象收到消息(观察者)响应变化。

怎么理解?

假设你喜欢炒股,需要及时关注一只股票的价格变化,刚开始你有空就手动查询一次,时间一长,感觉太麻烦,而且经常错过价格的波动,错失机会,怎么办?

如果能搞个监控,在股票价格符合阈值时,发个通知就好了。此时你就是观察者,股票是被观察者。

非观察者模式实现

在需求不复杂时,可以简单实现监控股票价格功能,写一个定时脚本,每分钟执行一次,脚本逻辑:

  1. 获取股票价格
  2. 价格满足阈值
  3. 发送通知

一段时间后,你的朋友听说你有这个好东西(需求变复杂),想让你帮忙监控几只股票。此时发生了 2 个变化:

  1. 被观察者变多了(更多的股票)
  2. 观察者变多了(增加了你的朋友)

上面的脚本要匹配新的需求,可能会修改成:

  1. 获取 A 股票价格
    1. 如果满足阈值 1,通知我
    2. 如果满足阈值 2,通知我朋友
  2. 获取 B 股票价格
    1. 如果满足阈值 3,通知我朋友
  3. 获取 C 股票价格
  4. ...

代码已经开始出现坏味道了...

又过了一段时间,更多的朋友知道了这件事,找你监控股票,改着改着,你崩溃了。

观察者模式实现

如何解决上面的困境?

问题的本质是不断增加的观察者和被观察者,如果只有一个观察者和被观察者就没有这么复杂,脚本简单写下就能搞定,如果不断增加,代码就会越来越复杂,最终难以维护。在业务上看,观察者的增加会比较频繁,被观察者可以看成独立的,因此问题可以简化为 GoF 所说的一对多关系。

我们把观察者和被观察者抽象出来:

  • 观察者的行为是在状态变化时收到通知
  • 被观察者的行为是在状态变化时发送通知

并且要建立两者之间的依赖关系,需要把观察者注册到被观察者的接口和观察者移除的接口。

由此可以得到:UML Class and Sequence diagram

观察者模式-20240806183547182.png

里面的 Subject(主题)就是被观察者。

interface Subject {
	func attach(Observer)
	func detach(Observer)
	func notify()
}
 
 
interface Observer {
	func update()
}

状态如何通知

这里有一个 trick 的点,就是状态变更后,当前的状态如何通知给 Observer。例如股票价格变化后,价格如何通知观察者。

第一种方式是 push 过去,在 update() 方法上增加状态参数,直接传入。这种方法的弊端是如果状态的定义变化,需要获取更多信息而修改函数签名,可能会导致一系列问题。

第二种方法是 pull 主动拉,这样需要在 update() 获取当前的 Subject,变成 update(Subject),并在 Subject 提供一个 getState 查询状态的方法。

我倾向于在变化少的场景使用 push 方式,在经常变化的场景使用 pull 的方式(其实也可以用 push,定义一个消息结构,方便添加字段),通过接口获取状态,更解耦。当然也可以两者结合起来使用。

Java 标准库中的观察者模式

在 Java 标准库中观察者模式应用及其广泛,Java Util 包中还提供了 java.util.Observable(Subject)和 java.util.Observer 接口以方便开发人员在编程过程中快速使用观察者模式。

package java.util
 
public interface Observer {
	void update(Observable o, Object arg);
}

这种方式把 push 和 pull 结合起来了,第二个参数 Object 用于传递变化的数据。

package java.util
 
public interface Observable {
	void addOberver(Observer o)
	void deleteOberver(Observer o)
	protected synchronized setChanged() { changed = true }
	protected synchronized clearChanged() { changed = false }
	void notifyObservers(Object arg) {
		Object[] arrLocal;
 
		synchronized (this) {
			if (!changed) return;
			arrLocal = obs.toArray() // 拷贝一份观察者进行通知
			clearChanged()
		}
 
		for (int i = arrLocal.length - 1; i>=0; i--) {
			((Observer)arrLocal[i]).update(this, arg);
		}
	}
}

setChanged 和 clearChanged 设置状态变化情况,在调用 notifyObservers 方法之前一定要调用 setChanged 方法,设置状态发生了改变,否则不会触发通知。

其它实现

Zustand 的核心原理也是该模式,一堆组件观察状态,状态变化时挨个通知组件更新。

为什么需要这个模式?

  1. 更好地管理需求变化,符合 OCP,新增观察者与被观察者,不需要修改原有代码,只要扩展新的类
  2. 更好地解耦,管理依赖,对观察者来说,只需要关心状态变化时得到的通知
  3. 是好莱坞原则的一个应用

资料

  • 《漫谈设计模式》
  • 【「观察者模式」与「发布/订阅模式」,你分得清楚吗?-哔哩哔哩】 https://b23.tv/oiMPYJ8