【一起学系列】之观察者模式:我没有在监控你啊
意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
别名:发布-订阅模式
观察者模式的诞生
将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的致性,我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。
说人话就是:
【产品】:开发小哥,我需要你设计一个天气预报显示大屏,气象站会给你发送数据,你需要把它展示到大屏里,OK吗?
【开发】:OJBK!秒秒钟搞定一切!代码立马出来!
void getTemperature () {
// 从气象站获取发送过来的温度数据
// getData();
// ................................
// 显示到大屏里面去
// showDataToScreen();
// ................................
}
void getMisture () {
// 从气象站获取发送过来的湿度数据
// getData();
// ................................
// 显示到大屏里面去
// showDataToScreen();
// ................................
}
void getAirindex () {
// 从气象站获取发送过来的空气指数数据
// getData();
// ................................
// 显示到大屏里面去
// showDataToScreen();
// ................................
}
【BOSS】:磕大头!宁是准备每获取一次数据就把代码CV一遍吗?你不累吗?
【开发】:老大,我一点都不累!就是复制粘贴一下呀!
【BOSS】:如果我现在不需要同步更新天气指数呢?删代码吗?
【开发】:对啊!一秒钟就能删掉!( •̀ ω •́ )✧
【BOSS】:重写😃
HeadFirst 核心代码
于是乎,我们开启了关于设计模式的经典书籍阅读之旅
/**
* 观察主题接口
*/
public interface Observable{
public void addObserver(Observer observer); // 添加观察者
public void removeObserver(Observer observer); // 移除观察者
public void notifyObservers(WeatherData data); // 通知所有观察者
}
/**
* 观察者
*/
public interface Observer {
public abstract void update(WeatherData data);
}
/**
* 天气主题
*
*/
public class Weather implements Observable {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void notifyObservers(WeatherData data) {
for (Observer observer : observers)
observer.update(data);
}
}
观察者模式的设计思路:
- Subject 目标(容器)提供注册和删除观察者的接口以及更新接口
- Observer(观察者)为那些在目标发生改变时需获得通知的对象定义一个更新接口
- ConcreteSubject(具体目标)状态发生改变时,向各个观察者发出通知
- ConcreteObserver(具体观察者)实现Observer的更新接口
简单来说,
- 我们需要一个接口来定义注册,删除和更新接口
- 然后由具体的目标(类)实现该接口,并且在类中创建一个容器,存储需要被通知的对象
- 需要被通知的对象,需要实现Observer接口中的update更新方法
- 将观察者对象注册进容器中,当具体目标更新时,调用所有容器类对象的update方法
如果看着有点模棱两可,就看完本文后,访问专题设计模式开源项目,里面有具体的代码示例,链接在最下面
JDK中的观察者模式
JDK中已经对观察者模式有具体的实现,代码非常简单,如下所示:
具体目标:
public class ObservableApp extends Observable {
private long curr;
public ObservableApp(long curr) {
this.curr = curr;
}
public void change(long newStr) {
this.curr = newStr;
// 更改状态,发送通知
setChanged();
notifyObservers(newStr);
}
@Override
protected synchronized void setChanged() {
super.setChanged();
}
}
具体观察者:
public class ObserverA implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println(MessageFormat.format("ObserverA -> {0} changed, Begin to Work. agr is:{1}", o.getClass().getSimpleName(), arg));
}
}
Main方法:
public class App {
public static void main(String[] args) throws InterruptedException {
ObservableApp app = new ObservableApp(System.currentTimeMillis());
System.out.println(app.getCurr());
app.addObserver(new ObserverA());
app.addObserver(new ObserverB());
Thread.sleep(1000);
long curr = System.currentTimeMillis();
app.change(curr);
System.out.println(app.getCurr());
}
}
// 输出如下:
// 1589688733464
// ObserverB -> ObservableApp changed, Begin to Work. agr is:1,589,688,734,469
// ObserverA -> ObservableApp changed, Begin to Work. agr is:1,589,688,734,469
// 1589688734469
推模式
通知发给观察者,通知携带参数,这就是推,对应JDK方法中的:notifyObservers(Object arg)
拉模式
通知发给观察者,通知不携带参数,需要观察者自己去主动调用get方法获取数据,这就是拉
对应JDK方法中的:notifyObservers(),仅告知观察者数据发生了变化,至于数据的详情需要观察者主动到主题中pull数据
拉模型强调的是目标不知道它的观察者,而推模型假定目标知道一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。另一方面。拉模型可能效率较差,因为观察者对象需在没有目标对象帮助的情况下确定什么改变了。
遵循的设计原则
- 封装变化
- 在观察者模式中会经常改变的是主题的状态,以及观察者的数目和类型
- 我们可以改变依赖于主题状态的对象,但是不必改变主题本身,这便是提前规划
- 针对接口编程
- 主题和观察者都使用了接口
- 观察者利用主题的接口向主题注册
- 主题利用观察者接口通知观察者,可以使两者之间正常交互,同时又具有松耦合的特性
- 多使用组合
- 观察者模式利用组合将许多观察者组合进主题中
- 它们之间的关系并不是通过继承得到,而是在运行时动态改变
什么场景适合使用
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern),比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式
Code/生活中的实际应用
- 比如微信公众号中的订阅关注,订阅后,公众号发布文章会实时分发给各个账号
- 又如,我们使用Keep跑步时,如果你跑的足够激情,它会提示你,恭喜你,你已经打破了五公里的最好记录!这样的语音提醒一定是触发式,而不是实时去检测吧?(实时检测没有意义,浪费性能)这里就可以利用观察者模式进行设计和解耦
最后
附上GOF一书中对于观察者模式的UML图:
相关代码链接
- 兼顾了《HeadFirst》以及《GOF》两本经典数据中的案例
- 提供了友好的阅读指导
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端