文章目录
- [1 基本介绍](#1 基本介绍)
- [2 案例](#2 案例)
-
- [2.1 Stock 抽象类](#2.1 Stock 抽象类)
- [2.2 StockA 类](#2.2 StockA 类)
- [2.3 Website 抽象类](#2.3 Website 抽象类)
- [2.4 WebsiteA 类](#2.4 WebsiteA 类)
- [2.5 Client 类](#2.5 Client 类)
- [2.6 Client 类的运行结果](#2.6 Client 类的运行结果)
- [2.7 总结](#2.7 总结)
- [3 各角色之间的关系](#3 各角色之间的关系)
-
- [3.1 角色](#3.1 角色)
-
- [3.1.1 Subject ( 被观察的主体 )](#3.1.1 Subject ( 被观察的主体 ))
- [3.1.2 ConcreteSubject ( 具体的主体 )](#3.1.2 ConcreteSubject ( 具体的主体 ))
- [3.1.3 Observer ( 观察者 )](#3.1.3 Observer ( 观察者 ))
- [3.1.4 ConcreteObserver ( 具体的观察者 )](#3.1.4 ConcreteObserver ( 具体的观察者 ))
- [3.1.5 Client ( 客户端 )](#3.1.5 Client ( 客户端 ))
- [3.2 类图](#3.2 类图)
- [4 注意事项](#4 注意事项)
- [5 在源码中的使用](#5 在源码中的使用)
- [6 优缺点](#6 优缺点)
- [7 适用场景](#7 适用场景)
- [8 总结](#8 总结)
1 基本介绍
观察者模式 (Observer Pattern)是一种 行为型 设计模式,它在对象之间建立 一对多 的依赖关系,使得 当一个对象的状态发生变化时 ,其所有依赖的对象都会得到通知并自动更新 。这个模式也称作 发布 - 订阅模式 ,多个对象 订阅 一个对象,当这个对象的内部状态发生变化时,它会 通知 多个对象,让它们更新有关这个对象的状态。
2 案例
本案例实现了一个观测股票价值的网站,当股票的价值变化时,更新网站中存储的股票价值。
2.1 Stock 抽象类
java
import java.util.ArrayList;
import java.util.List;
public abstract class Stock { // 股票
private List<Website> websites = new ArrayList<>(); // 储存所有观测股票价值的网站
public void addWebsite(Website website) { // 添加网站
websites.add(website);
}
public void deleteWebsite(Website website) { // 删除指定网站
websites.remove(website);
}
public void notifyWebsites() { // 通知所有网站修改本股票的数据
for (Website website : websites) {
website.update(this);
}
}
protected int value; // 股票的价值
private String name; // 股票的名称
public Stock(String name) {
this.name = name;
}
public int getValue() { // 获取股票的价值
return value;
}
public String getName() { // 获取股票的名称
return name;
}
public abstract void simulate(); // 模拟股票价值的跌涨,具体实现交给子类
}
2.2 StockA 类
java
import java.util.Random;
public class StockA extends Stock { // 股票A
public StockA() {
super("股票A");
}
private Random random = new Random();
@Override
public void simulate() {
// 每隔 1 秒修改一次股票A的价值,共修改 10 次,修改完后通知各个网站修改数据
for (int i = 0; i < 10; i++) {
value = random.nextInt(100 + value) + 10; // 保底价值为 10
notifyWebsites();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2.3 Website 抽象类
java
public abstract class Website { // 能查看股票情况的网站
public abstract void update(Stock stock); // 更新指定股票的数据
// 显示股票的数据,这个方法可以被外部直接调用,不过此时显示的是缓存的数据
public abstract void print();
}
2.4 WebsiteA 类
java
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class WebsiteA extends Website { // 监测股票数据的网站A
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 缓存股票的信息,key 是股票的名称,value 是股票的价格
private Map<String, Integer> stocks = new HashMap<>();
@Override
public void update(Stock stock) {
stocks.put(stock.getName(), stock.getValue());
print(); // 更新完数据之后打印一次信息
}
@Override
public void print() {
System.out.println("==============「网站A」==============");
for (Map.Entry<String, Integer> entry : stocks.entrySet()) {
System.out.println(SDF.format(new Date())
+ " [" + entry.getKey() + "]的价格为:" + entry.getValue());
}
}
}
2.5 Client 类
java
public class Client { // 客户端,测试了 网站A 监测 股票A 的跌涨
public static void main(String[] args) {
Stock stockA = new StockA();
WebsiteA websiteA = new WebsiteA();
stockA.addWebsite(websiteA);
stockA.simulate();
}
}
2.6 Client 类的运行结果
==============「网站A」==============
2024-08-17 00:10:12 [股票A]的价格为:74
==============「网站A」==============
2024-08-17 00:10:13 [股票A]的价格为:110
==============「网站A」==============
2024-08-17 00:10:14 [股票A]的价格为:199
==============「网站A」==============
2024-08-17 00:10:15 [股票A]的价格为:289
==============「网站A」==============
2024-08-17 00:10:16 [股票A]的价格为:321
==============「网站A」==============
2024-08-17 00:10:17 [股票A]的价格为:89
==============「网站A」==============
2024-08-17 00:10:18 [股票A]的价格为:174
==============「网站A」==============
2024-08-17 00:10:19 [股票A]的价格为:129
==============「网站A」==============
2024-08-17 00:10:20 [股票A]的价格为:135
==============「网站A」==============
2024-08-17 00:10:21 [股票A]的价格为:143
2.7 总结
本案例中,将股票的数据缓存到网站中,一旦股票的价值发生变化,股票就通知所有订阅这支股票的网站,让它们修改股票数据,一致性比较强 (这里的一致性指的是 股票的真实价值 和 网站中缓存的价值 是相同的),这就是 发布 - 订阅 模型的优点。
如果想要再添加 一支股票 或 一个网站,则只需要继承对应的抽象类,并实现方法,然后就可以让网站订阅股票,股票一旦修改数据,订阅的网站将会更新股票的数据。可以发现,这个系统的扩展性比较强。
3 各角色之间的关系
3.1 角色
3.1.1 Subject ( 被观察的主体 )
该角色负责定义三类接口:
- 注册 和 删除 Observer 角色。
- 通知所有已注册的 Observer 角色。
- 获取当前状态。
本案例中,Stock
抽象类扮演了该角色。
3.1.2 ConcreteSubject ( 具体的主体 )
该角色负责 实现 Subject 角色定义的 接口 。本案例中,StockA
类扮演了该角色。
3.1.3 Observer ( 观察者 )
该角色负责 定义 更新本地缓存的旧数据 的接口 ,当 Subject 角色的状态发生变化时,它会调用这个接口来通知所有的 Observer 角色。本案例中,Website
抽象类扮演了该角色。
3.1.4 ConcreteObserver ( 具体的观察者 )
该角色负责 实现 Observer 角色定义的 接口 。本案例中,WebsiteA
类扮演了该角色。
3.1.5 Client ( 客户端 )
该角色负责 创建合适的 ConcreteSubject 角色和 ConcreteObserver 角色 ,使用 Subject 角色和 Observer 角色完成具体的业务逻辑 。本案例中,Client
类扮演了该角色。
3.2 类图
说明:Subject 和 Observer 都可以使用接口实现,不过这时在 Subject 中就无法实现与 Observer 相关的三个方法了。
4 注意事项
- 定义清晰的接口:确定 Observer 接口,包括需要通知的方法以及可能的参数。这样可以确保 Subject 和 Object 之间的松耦合关系,并提供灵活性。
- 避免滥用:观察者模式适用于 Subject 和 Observer 之间的一对多关系,如果仅涉及两个对象之间的通信,使用观察者模式可能过于复杂。本案例中没有明显体现出这一点,这是为了避免案例过于复杂。
- 注销通知 :在 Subject 和 Observer 进行销毁时,都要向对方发送通知 。这有助于 避免在对象销毁后还尝试进行通信,从而引发错误或异常。
- 消息通知顺序 :当多个 Observer 监听同一个 Subject 时,Observer 接收通知的顺序可能会影响系统行为。需要明确观察者的通知顺序,以确保正确的处理顺序。
- 谨慎处理循环依赖 :在 Subject 通知 Observer 时,如果 Observer 也改变了 Subject 的状态,接着 Subject 再通知 Observer,会导致无限循环。必要时,可以考虑使用 标志 或其他机制来避免循环依赖。
- 观察者生命周期管理 :需要 及时 注册 和 删除 观察者 ,以避免 资源泄漏 或 潜在的内存问题。
- 线程安全性 :如果在 多线程环境 下使用观察者模式,需要 确保 Subject 和 Observer 的并发访问是线程安全的 ,可以考虑使用 同步机制 或 线程安全的集合 来避免数据竞争和状态不一致的问题。
- 性能优化 :如果 Observer 的处理逻辑耗时较长,可能会影响 Subject 的性能。可以使用 异步 或 延迟通知 的方式来优化性能,确保通知方法尽快返回控制权,避免阻塞其他操作。如果对 Observer 的通知是通过另外的线程进行 异步投递 的,系统需要避免 消息丢失 或 重复处理。
5 在源码中的使用
在 JDK 中,java.util.Observer
接口 和 java.util.Observable
类 是实现观察者模式的基础。其对应的角色如下所示:
-
Subject 角色 :
Observable
类,这个类中实现了基础的两类方法(缺少了 获取本对象状态 的方法):javapublic class Observable { private boolean changed = false; // 标记本对象的状态是否改变 private Vector<Observer> obs; // 存储所有 Observer 的变长数组 public Observable() { obs = new Vector<>(); } // 注册 Observer public synchronized void addObserver(Observer o) { if (o == null) throw new NullPointerException(); if (!obs.contains(o)) { obs.addElement(o); } } // 删除 Observer public synchronized void deleteObserver(Observer o) { obs.removeElement(o); } // 删除所有 Observer public synchronized void deleteObservers() { obs.removeAllElements(); } // 获取已注册的 Observer 个数 public synchronized int countObservers() { return obs.size(); } // 当本对象的状态变化时,通知所有的 Observer public void notifyObservers() { notifyObservers(null); } // 当本对象的状态变化时,通知所有的 Observer public void notifyObservers(Object arg) { // 存储当前所有的 Observer Object[] arrLocal; synchronized (this) { // 如果本对象的状态没有变化,则无需通知 Observer if (!changed) return; arrLocal = obs.toArray(); // 获取当前所有的 Observer clearChanged(); // 清除改变标记 } // 倒序通知所有的 Observer for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } // 标记本对象的状态被改变,由其子类调用 protected synchronized void setChanged() { changed = true; } // 清除改变的标记,由本类调用 protected synchronized void clearChanged() { changed = false; } // 检查本对象的状态是否改变 public synchronized boolean hasChanged() { return changed; } }
-
ConcreteSubject 角色 :继承
Observable
类的子类就是 ConcreteSubject,一般要调用其父类的setChanged()
方法,并且还需要实现 获取本对象状态的方法。 -
Observer 角色 :
Observer
接口,这个接口中定义了update()
方法:javapublic interface Observer { void update(Observable o, Object arg); }
-
ConcreteObserver 角色 :实现
Observer
接口的类就是 ConcreteObserver,在 Subject 发生变化时,调用update()
方法。
6 优缺点
优点:
- 增强了数据的一致性 :当 Subject 的状态发生变化时,它可以通知所有已注册的 Observer。这种广播机制使得多个 Observer 能够 同时响应状态变化 ,增强了数据的 一致性。
- 降低系统的耦合度 :观察者模式实现了 Subject 和 Observer 之间的松耦合,这意味着 Subject 和 Observer 可以独立地改变和复用,而不需要修改对方。这种松耦合使得系统更加 灵活 和 易于维护。
- 增强系统的扩展性 :可以在运行时动态地 添加 或 删除 观察者,而不需要修改主题类的代码。这种灵活性使得系统能够更容易地适应变化。
- 符合开闭原则:观察者模式对扩展开放,对修改关闭。当需要增加新的 ConcreteObserver 时,只需要实现(或 继承)Observer 并注册到 Subject 中即可,而不需要修改 Subject 的代码。
缺点:
- 性能问题 :如果一个 Subject 拥有 大量的 Observer 时,并且 状态更新非常频繁 ,那么每次状态更新时, Subject 都需要遍历整个 Observer 列表并通知它们,这可能会导致性能问题,特别是在 高并发 场景下。
- 可能导致循环依赖 :如果 Observer 和 Subject 之间存在相互依赖的关系,那么可能会导致循环依赖问题。这种情况下,系统可能会出现 死锁 或 无限递归 等问题。
- 难以实现异步通信 :观察者模式通常是在 同步方式 下工作的,即 Subject 在状态更新后 立即 通知 Observer。在某些情况下,如果 Observer 需要花费较长时间来处理状态更新,那么这可能会阻塞 Subject 或 其他 Observer 的执行。虽然可以通过一些技术手段(如使用 多线程 或 异步消息队列)来实现异步通信,但这会增加系统的复杂性和实现难度。
7 适用场景
- 消息发布 - 订阅系统 :观察者模式可以用于 构建 消息发布 - 订阅系统 。在这种系统中,消息发布者充当 Subject ,而订阅者则充当 Observer。当发布者发布新消息时,所有订阅者都会收到通知并执行相应操作。例如 新闻订阅服务、实时数据监控系统 等。
- 图形用户界面(GUI)开发 :在 GUI 开发中,观察者模式常被用于 处理用户界面组件之间的交互。当一个组件的状态发生变化时,其他依赖该组件的组件将自动更新以反映新的状态。例如 按钮点击事件、窗口状态变化 等。
- 事件驱动系统 :观察者模式也常用于 事件驱动系统 中,如图形用户界面框架、游戏引擎等。当 特定事件发生 时,触发相应的回调函数并通知所有注册的观察者。
- 实时数据更新 :在需要 实时更新数据的应用 中,观察者模式可以用于 将 数据源 与 数据消费者 连接起来。当数据源的数据发生变化时,观察者可以自动获取最新的数据并进行处理。例如 实时天气更新、股票价格实时推送 等。
- 消息队列系统 :观察者模式可用于 消息队列系统 ,其中 生产者 将消息发送到队列,而 消费者 作为 Observer 订阅队列以接收和处理消息。
- 分布式系统数据同步 :在分布式系统中,可以使用观察者模式实现 节点之间的数据同步。当任何一个节点的状态发生变化时,它会通知其他的节点进行相应的更新操作。例如 分布式数据库、缓存系统 等。
8 总结
观察者模式 是一种 行为型 设计模式,它在对象间建立了 一对多 的关系,当 一 的状态发生变化时,一 会通知 多 ,更新 多 缓存的状态,从而实现 一 与 多 之间的强一致性。但是在 多 的数量太大 且 更新 多 缓存状态的操作很耗时 的情况下,系统的性能会比较低。此外,在使用本模式时,要注意 Subject 和 Observer 之间最好不要循环调用,因为这样可能会造成无限递归的问题。