一、模式介绍
1.1、定义
观察者(Observe)模式定义了对象之间的一对多的依赖,这样一来,当一个对象改变状态是,它的所有依赖者都会收到通知并自动更新。观察者模式提供一个对象设计,让主题和观察者之间松耦合。
这种模式有时又称作发布-订阅模式,出版者(主题)+ 订阅者(观察者)= 观察者模式。实现观察者模式时,要主要具体目标对象和具体观察者对象之间不能直接调用,否则会使两者之间紧密耦合起来,这违反了面向对象的设计原则。
1.2、优点
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
- 目标与观察者之间建立了一套触发机制
1.3、缺点
- 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用
- 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率
二、结构与实现
2.1、结构
- 抽象主题(Subject)角色:也叫抽象目标类,对象使用此接口注册为观察者,或把自己从观察者中删除,以及通知所有观察者的抽象方法。每个主题可以有许多观察者
- 具体主题(ConcreteSubject)角色:也叫具体目标类,一个具体主题总是实现主题接口,除了注册和撤销方法之外,还实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。具体的主题也可能有设置和获取状态的方法
- 抽象观察者(Observer)角色:它是一个抽象类或接口,所有潜在的观察者必须实现这个接口;它包含了一个更新自己的抽象方法,当主题状态改变时,被调用
- 具体观察者(ConcreteObserver)角色:具体的观察者实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。观察者必须注册具体主题,以便接收更新
2.2、实现一:自己实现 Observer
设计一个气象站应用,有三种布告分别显示目前的状况、气象统计及简单的预报,三种必须实时更新,而且这是一个可以扩展的气象站,可以根据气象站的API,写出自己的气象公告板。
2.2.1、类图
2.2.2、Subject
package com.erlang.subject.my;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 抽象主体
* @author: erlang
* @since: 2022-02-18 08:09
*/
public abstract class Subject {
/**
* 记录观察者,此List 是在构造器中建立
*/
protected List<Observer> observers;
public Subject() {
observers = new ArrayList<>();
}
/**
* 当注册观察者时,我们只要把它放在List 集合中即可
*
* @param observer 观察者
*/
public void registerObserver(Observer observer) {
observers.add(observer);
}
/**
* 管观察者取消订阅,我们把它从List 中删除即可
*
* @param observer 观察者
*/
public void removeObserver(Observer observer) {
int i = observers.indexOf(observer);
if (i > 0) {
observers.remove(observer);
}
}
/**
* 当主题状态改变时,这个方法会被调用,已通知所有的观察者
*/
public abstract void notifyObservers();
}
2.2.3、Observer
package com.erlang.subject.my;
/**
* @description: 自己实现的观察者接口
* 所有观察者都必须实现此方法,以实现观察者接口
* @author: erlang
* @since: 2022-02-18 08:10
*/
public interface Observer {
/**
* 当气象观测值改变时,主题会把这些状态值当作方法的餐朱,传给观察者
*
* @param temperature 温度
* @param humidity 湿度
* @param pressure 气压
*/
void update(float temperature, float humidity, float pressure);
}
2.2.4、DisplayElement
package com.erlang.subject.my;
/**
* @description: 当布告板需要显示时,需要实现此接口
* @author: erlang
* @since: 2022-02-18 08:11
*/
public interface DisplayElement {
/**
* 当布告板需要显示时,调用此方法
*/
void display();
}
2.2.5、CurrentConditionsDisplay
package com.erlang.subject.my;
/**
* @description: 此布告实现了IObserver 接口,可以从 WeatherData 对象中获得改变
* 也实现了 DisplayElement 接口,因为我们的 API 规定所有的布告都必须实现此接口
* @author: erlang
* @since: 2022-02-18 08:12
*/
public class CurrentConditionsDisplay implements DisplayElement, Observer {
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 气压
*/
private float pressure;
/**
* 主体
*/
private Subject subject;
/**
* 可以在创建该对象时,就注册观察者
*
* @param subject
*/
public CurrentConditionsDisplay(Subject subject) {
this.subject = subject;
}
/**
* 注册观察者
*/
public void registerObserver() {
subject.registerObserver(this);
}
/**
* 当此方法被调用时,我们把温度和湿度保存起来,然后调用display()
*
* @param temperature 温度
* @param humidity 湿度
* @param pressure 气压
*/
@Override
public void update(float temperature, float humidity, float pressure) {
System.out.println();
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
/**
* 展示最近的温度和湿度
*/
@Override
public void display() {
System.out.println("当前天气情况:气温 " + temperature + " 度,湿度:" + humidity + "%,气压:" + pressure);
System.out.println("--------------------------------------------------------------------------------------");
}
}
2.2.6、WeatherData
package com.erlang.subject.my;
/**
* @description: 气象台
* @author: erlang
* @since: 2022-02-18 08:14
*/
public class WeatherData extends Subject {
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 气压
*/
private float pressure;
/**
* 把状态告诉每一个观察者,因为所有观察者都是实现了update() 方法,所以知道如何通知他们
*/
@Override
public void notifyObservers() {
if (observers.size() > 0) {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
}
/**
* 当气象站得到更新观测值时,我们通知观察者
*/
public void measurementsChanged() {
notifyObservers();
}
/**
* 设置温度
*
* @param temperature 温度
* @param humidity 湿度
* @param pressure 气压
*/
public void setMeasurements(float temperature, float humidity, float pressure) {
System.out.println("#### WeatherData.setMeasurements:天气变化通知...");
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
2.2.7、启动气象站
package com.erlang.subject.my;
/**
* @description: 气象站
* @author: erlang
* @since: 2022-02-18 08:18
*/
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay display = new CurrentConditionsDisplay(weatherData);
// 注册观察者
display.registerObserver();
// 模拟新的气象测量
weatherData.setMeasurements(30, 65, 30.1f);
weatherData.setMeasurements(29, 67, 32.1f);
weatherData.setMeasurements(27, 63, 31.1f);
}
}
2.2.8、运行结果
#### WeatherData.setMeasurements:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 30.0 度,湿度:65.0%,气压:30.1
#### WeatherData.setMeasurements:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 29.0 度,湿度:67.0%,气压:32.1
#### WeatherData.setMeasurements:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 27.0 度,湿度:63.0%,气压:31.1
2.3、实现二:Java 内置的 Observer
2.3.1、Observer
对象实现该接口称为观察者。观察者接收数据的动作子类对 update 的实现,其中第一个参数是主题本身,是让观察者知道是哪个主题通知它;第二个参数是 Observable::notifyObservers 方法的入参,如果没有说明则为空。
// java.util.Observer
public interface Observer {
void update(Observable o, Object arg);
}
2.3.2、Observable 部分代码
观察者注册见 addObserver 方法;删除见 Observable::deleteObserver 方法。可观察者发出通知的动作见 notifyObservers 方法,在调用该方法前需要先调用 setChanged,标记可观察者的状态为允许发送状态。
public class Observable {
// ....
// 注册观察者
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
// 删除观察者
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
// 发送给所有观察者
public void notifyObservers() {
notifyObservers(null);
}
// 发送指定数据给所有的观察者
public 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);
}
// 标记状态已改变,允许发送通知给观察者
protected synchronized void setChanged() {
changed = true;
}
// ....
}
2.3.3、类图
2.3.4、WeatherData
package com.erlang.subject.sys;
import java.util.Observable;
/**
* @description: 继承 Observable 类
* 这里不再需要追踪观察者了,也不需要管理观察者的注册与删除(父类中已经实现了这些功能)
* @author: erlang
* @since: 2022-02-18 08:26
*/
public class WeatherData extends Observable {
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 气压
*/
private float pressure;
public WeatherData() {
}
/**
* 当气象站得到更新观测值时,我们通知观察者
*/
public void measurementsChanged() {
System.out.println("#### WeatherData.measurementsChanged:天气变化通知...");
// 调用 notifyObservers() 方法之前需要先调用setChanged() 方法,来指示状态已经改变
setChanged();
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
2.3.5、CurrentConditionsDisplay
package com.erlang.subject.sys;
import com.erlang.subject.my.DisplayElement;
import java.util.Observable;
import java.util.Observer;
/**
* @description: 此布告实现了java.util.Observer 接口
* 也实现了 DisplayElement 接口,因为我们的 API 规定所有的布告都必须实现此接口
* @author: erlang
* @since: 2022-02-18 08:27
*/
public class CurrentConditionsDisplay implements Observer, DisplayElement {
/**
* 温度
*/
private float temperature;
/**
* 湿度
*/
private float humidity;
/**
* 气压
*/
private float pressure;
/**
* 观察者中心
*/
private Observable observable;
/**
* 可以在创建该对象时,就注册观察者
*
* @param observable
*/
public CurrentConditionsDisplay(Observable observable) {
this.observable = observable;
}
/**
* 注册观察者
*/
public void registerObserver() {
observable.addObserver(this);
}
/**
* 当此方法被调用时,我们把温度和湿度保存起来,然后调用display()
*
* @param o
* @param arg
*/
@Override
public void update(Observable o, Object arg) {
System.out.println("#### CurrentConditionsDisplay.update:接收到天气变化通知...");
if (o instanceof WeatherData) {
WeatherData weatherData = (WeatherData) o;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
this.pressure = weatherData.getPressure();
display();
}
}
/**
* 展示最近的温度和湿度
*/
@Override
public void display() {
System.out.println("#### CurrentConditionsDisplay.display:当前天气情况:气温 " + temperature + " 度,湿度:" + humidity + "%,气压:" + pressure);
System.out.println("--------------------------------------------------------------------------------------");
}
}
2.3.6、WeatherStation
package com.erlang.subject.sys;
/**
* @description: 气象站
* @author: erlang
* @since: 2022-02-18 08:30
*/
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
// 注册观察者
currentDisplay.registerObserver();
/**
* 模拟新的气象测量
*/
weatherData.setMeasurements(30, 65, 30.1f);
weatherData.setMeasurements(29, 67, 32.1f);
weatherData.setMeasurements(27, 63, 31.1f);
}
}
2.3.7、执行结果
#### WeatherData.measurementsChanged:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 30.0 度,湿度:65.0%,气压:30.1
--------------------------------------------------------------------------------------
#### WeatherData.measurementsChanged:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 29.0 度,湿度:67.0%,气压:32.1
--------------------------------------------------------------------------------------
#### WeatherData.measurementsChanged:天气变化通知...
#### CurrentConditionsDisplay.update:接收到天气变化通知...
#### CurrentConditionsDisplay.display:当前天气情况:气温 27.0 度,湿度:63.0%,气压:31.1
--------------------------------------------------------------------------------------