观察者模式——学习笔记
观察者模式
介绍
观察者模式是极其重要的一个设计模式,在许多框架都使用了,以及实际开发中也会用到。
定义对象之间的一种一对多的依赖关系,使得每当一个对象的状态发生变化时,其相关的依赖对象都可以得到通知并被自动更新。主要用于多个不同的对象对一个对象的某个方法会做出不同的反应!
以不同观察者从同一个天气台中获取数据为例,实践一下简单而普通的观察者模式实现。在(23GoF)书中也有这个例子,这里借鉴了Observable 进行了一些修改。
其核心在于理解它的定义、使用场景、以及四个重要的方法:
addObserver(Observer o);
addObserver(Observer o);
addObserver(Observer o);
update(Object o);
update 由 观察者实现,其余的由主题实现。
代码示例
1. Subject 主题接口
/**
* @Author CNDA
* @Date 2023/3/14 8:42
* @ClassName: Subject
* @Description: Subject 主题,一旦有改变则推送给所有已经订阅的观察者或者观察者自己拉取数据
* 注意:这个方案只是做学习,并不适用于多线程以及同步的情况。
* @Version 1.0
*/
public interface Subject {
void addObserver(Observer o); // 添加观察者到列表中
void removeObserver(Observer o); // 从列表中输出观察者
void notifyObserver(Object arg); // 将 arg 的数据发送给所有观察者
int size(); // 返回当前主题的所有订阅者个数
}
2. Observer 观察者接口/抽象类 这里是接口实现
public interface Observer {
/**
* 当主题有变化,会通知所有订阅的 Observer
* @param arg 主题变化的数据
* 观察者可以主动 ”拉取“ 或者 由主题 ”推送“
*/
void update(Object arg);
}
3. 实现 Subject 接口类——WeatherData 类
天气主题为例。
public class WeatherData implements Subject {
private float temperature;
private float humidity;
private float light;
private float co_2;
private boolean isPush = true; // true 为主动推送,false 为订阅者调用对应getxxx() 方法拉取数据。
// 观察者列表
private final List<Observer> observers;
// 无参构造
public WeatherData() {
this.observers = new ArrayList<>();
}
// 提供 getter() 给观察者拉取数据。
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getLight() {
return light;
}
public float getCo_2() {
return co_2;
}
public void setPush(boolean isPush){
this.isPush = isPush;
}
@Override
public void addObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(o);
}
}
@Override
public void notifyObserver(Object arg) {
// 通知 observers 的所有观察者
for (Observer observer : observers) {
observer.update(arg);
}
}
// 设置内容、更新数据。
public void setManager(float temperature, float humidity, float light, float co_2){
if (isPush){
Map<String,Float> pramMap = new HashMap<>();
pramMap.put("temperature",temperature);
pramMap.put("humidity",humidity);
pramMap.put("light",light);
pramMap.put("co_2",co_2);
push(pramMap);
}else {
this.temperature = temperature;
this.humidity = humidity;
this.light = light;
this.co_2 = co_2;
pull();
}
}
public void pull(){
notifyObserver(null);
}
public void push(Map<String,Float> map){
notifyObserver(map);
}
@Override
public int size() {
return observers.size();
}
// 清理 Observers
public void clearObservers(){
observers.clear();
}
}
通常基础的观察者模式的落地实现,一般会有一个变量控制:booble change。
change 表示标记已经改变的事实。
在 java.util.Observable 中就有一个这个变量,其主要作用是由主题来规定是否更新数据。
可能有些值需要一定的量变才能代表其数据价值。并不是每次变动都要通知所有订阅者更新数据。
而观察者更新数据的方式又分为 主动推送(push) 和 观察者拉取(pull):
如果有一定量的数据,并不是每个观察者都需要主题推送所有数据,可能只需要个别数据,这个时候就可以向外部暴露接口,让观察者拉取所需要的数据。
4. 实现 Observer 接口类 —— DefaultObserver、BaseObserver
4.1 DisplayElement
public interface DisplayElement {
void display();
}
所有 Observer 都需要实现这个方法,用于显示打印不同观察者的内容。代表观察者的数据内容展示。
4.2 BaseObserver
使用的是推送数据。
public class BaseObserver implements Observer, DisplayElement {
private float temperature;
private float humidity;
private float light;
private float co_2;
private Subject subject;
@Override
public void update(Object arg) {
// 解析 arg
if (arg instanceof HashMap){
Map<String, Float> map = (Map<String, Float>) arg;
this.temperature = map.get("temperature");
this.co_2 = map.get("co_2");
this.light = map.get("light");
this.humidity = map.get("humidity");
}
display();
}
@Override
public void display() {
System.out.println("BaseObserver Context temperature : " + this.temperature
+ " humidity : " + this.humidity + " light : " + this.light + " Co_2 : " + this.co_2);
}
public BaseObserver(Subject subject) {
this.subject = subject;
}
// 取消订阅
public void unsubscribe() {
subject.removeObserver(this);
}
// 设置主题
public void setSubject(Subject subject) {
this.subject = subject;
}
}
4.3 DefaultObserver
使用的是拉取数据
public class DefaultObserver implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject subject;
public DefaultObserver(Subject subject) {
this.subject = subject;
}
@Override
public void update(Object arg) {
// 如果 arg 是 null 则主动拉取
if (subject instanceof WeatherData) {
WeatherData weatherData = (WeatherData) subject;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
}
display();
}
public void unsubscribe() {
subject.removeObserver(this);
}
// 重置 Subject 引用
public void setSubject(Subject subject) {
this.subject = subject;
}
@Override
public void display() {
System.out.println("DefaultObserver Context temperature : " + this.temperature + " humidity : " + this.humidity);
}
}
5. 测试效果
@Test
public void test01(){
// 主题
WeatherData weatherData = new WeatherData();
// 准备两个观察者
DefaultObserver observer = new DefaultObserver(weatherData);
BaseObserver baseObserver = new BaseObserver(weatherData);
// 将观察者订阅到列表中
weatherData.addObserver(observer);
weatherData.addObserver(baseObserver);
// 主题更新数据
weatherData.setManager(32.5f,20.6f,155.6f,16.9f);
weatherData.setManager(30.2f,26.9f,504.6f,19.9f);
weatherData.setManager(28.9f,10.6f,400.1f,30.9f);
}
已知主题默认是推送:
只有 BaseObserver
才有数据更新。
设置 weatherData.setPush(false);
改为由观察者拉取数据。
注意:上面将拉取和推送单独分开了。从而导致在推送时,拉取不到新的数据,反之亦然。
5.1 测试观察者取消订阅
// 主题更新数据
weatherData.setManager(32.5f,20.6f,155.6f,16.9f);
observer.unsubscribe();
weatherData.setManager(30.2f,26.9f,504.6f,19.9f);
weatherData.setManager(28.9f,10.6f,400.1f,30.9f);
结果符合预期。
5.2 测试主题清空观察者列表
// 主题更新数据
weatherData.setManager(32.5f,20.6f,155.6f,16.9f);
weatherData.clearObservers();
weatherData.setManager(30.2f,26.9f,504.6f,19.9f);
weatherData.setManager(28.9f,10.6f,400.1f,30.9f);
JDK 中的 Observable 可观察者
上面的代码是在学习了观察者模式以及 Observable 之后进行一些修改的。
JDK 中有观察者模式的实现方案:Observable
和 Observer
。但是在 JDK 9 中被废弃了,因为存在一些问题。其中最主要的是 Observable
是一个类,不易于扩展,以及各种安全问题。而 Observer
又依赖于 Observable
。
所以这些问题就是 Observable
的黑暗面。
在 Swing 中,也有运用到过观察者模式。
总结
简单来说观察者模式是典型的一对多结构。观察者模式可以方便统一的更新订阅者的数据,观察者可以从主题中获取对应的数据,不关心数据如何产生,以及如何传递,只关心如何使用这些数据。而主题只维护好观察者列表,当数据更新时根据业务情况去通知所有订阅的观察者。达到松耦合、可扩展、易维护的目的。
自己对于模式的一个浅层理解和简单代码实现,可以根据自己的想法围绕着该模式的定义进行设计,也可以看其他优秀的博客和优质的代码,学习精华和思想。