设计模式:观察者模式
观察者设计模式
定义对象间一对多的依赖关系,当一个对象的状态发生时,所有依赖于它的对象都会得到通知并自动更新,使自己的状态和目标对象保持一致。
观察者设计模式的结构
- 目标对象:
- 目标对象有多个观察者观察;
- 目标对象维护对观察者的注册和退订;
- 目标对象的状态发生变化时,负责通知所有有效的注册的观察者们;
- 观察者:提供目标对象通知时的更新方法,执行对应的业务逻辑处理
- 具体的目标对象:维护目标状态,当目标状态发生改变时,主动触发通知所有的观察者们
- 具体的观察者对象:接收目标状态变更的通知,进行相应的处理
观察者设计模式的场景
示例场景:读者订阅报纸。 读者首先向报社发起订阅请求,报社负责维护读者列表(订阅或者退订),报纸是具体的目标对象,读者则可以被认为是具体的观察者。
观察者类关系图
观察者设计模式的场景实现
首先我们来定义抽象的目标对象:报社,即抽象的目标对象
Java
public interface Subject {
/**
* 注册观察者
*
* @param observer 观察者
*/
void attach(Observer observer);
/**
* 移除观察者
*
* @param observer 观察者
*/
void detach(Observer observer);
/**
* 通知观察者
*/
void notifyObservers();
/**
* 通知观察者
*
* @param content 变更内容
*/
void notifyObservers(String content);
}
接着我们定义一个具体的目标对象:报纸
Java
public class Newspaper implements Subject {
/**
* 报纸内容
*/
private String content;
/**
* 维护读者列表
*/
private final List<Observer> readers = new ArrayList<>();
/**
* 获取内容
*
* @return 内容
*/
public String getContent() {
return content;
}
/**
* 内容更新,通知观察者们
*
* @param content 内容
*/
public void setContent(String content) {
this.content = content;
// 拉模型,传递目标对象本身,观察者拿到目标对象后,查询目标对象的变更内容
this.notifyObservers();
// 推模型,主动推送变更的内容
this.notifyObservers(content);
}
/**
* 注册观察者
*
* @param observer 观察者
*/
@Override
public void attach(Observer observer) {
if (observer != null && !readers.contains(observer)) {
readers.add(observer);
}
}
/**
* 移除观察者
*
* @param observer 观察者
*/
@Override
public void detach(Observer observer) {
readers.remove(observer);
}
/**
* 通知观察者: 拉模型
*/
@Override
public void notifyObservers() {
this.readers.forEach(item -> item.update(this));
}
/**
* 通知观察者: 推模型
*/
@Override
public void notifyObservers(String content) {
this.readers.forEach(item -> item.update(content));
}
}
有了具体的目标对象,那么我们需要再定义下观察者,观察者众多,而目标对象不可能每个都单独维护,多态的好处就体现出来了,我们只需要定义抽象的观察者
Java
public interface Observer {
/**
* 拉模型
*
* @param subject 目标对象
*/
void update(Subject subject);
/**
* 推模型
*
* @param content 具体内容
*/
void update(String content);
}
最后我们再来定义具体的观察者:读者
Java
public class Reader implements Observer {
private final String name;
public Reader(String name) {
this.name = name;
}
/**
* 执行更新逻辑,拉模型,拿到目标对象之后主动获取想要的内容
*
* @param subject 目标对象
*/
@Override
public void update(Subject subject) {
System.out.println(this.name + "阅读(拉模型):" + ((Newspaper)subject).getContent());
}
/**
* 执行更新逻辑,推模型,通知啥就只能拿到啥,拿不到更多的内容
*
* @param content 具体内容
*/
@Override
public void update(String content) {
System.out.println(this.name + "阅读(推模型):" + content);
}
}
我们来测试一下,当报纸内容更新时,看下读者们是否可以收到通知
Java
public class Client {
public static void main(String[] args) {
Reader zhangsan = new Reader("张三");
Reader lisi = new Reader("李四");
Reader wangwu = new Reader("王五");
Newspaper newspaper = new Newspaper();
newspaper.attach(zhangsan);
newspaper.attach(lisi);
newspaper.attach(wangwu);
newspaper.setContent("本期内容: 观察者模式");
}
}
JDK 中的观察者设计模式
其实 JDK 早在 1.0 版本就已经为我们定义了观察者设计模式需要的抽象目标和观察者接口,可以在 java.util 包中找到。
- 抽象目标对象: java.util.Observable
- 观察者接口:java.util.Observer
使用 JDK 定义的观察者模式的抽象目标类和接口修改示例,和上面的示例不同的是:
- 上述示例具体目标类是实现了抽象目标接口,而 JDK 提供的抽象目标是类,需要改成继承,且观察者的纳管也在父类完成;
- 我们具体的目标对象在更新内容后且触发观察者更新之前需要先调用以下步骤 2 的方法;
- 触发观察者更新时传递的对象,内部即传递了目标对象本身,也传递了详细的变更内容。
Java
// 内容更新,通知观察者们
public void setContent(String content) {
// 1. 先更新内容
this.content = content;
// 2. 改变 change 状态
this.setChanged();
// 3. JDK 推拉一体
this.notifyObservers(content);
}
执行步骤 2 的原因在于状态未改变时,不通知观察者们
Java
// 拉模型
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);
}
JDK 推拉一体,实际上就是定义的抽象观察者接口中的更新接口,同时具有目标对象本身,也可以传递具体变更内容,使用者自行选择使用方式。
Java
public interface Observer {
void update(Observable o, Object arg);
}
Observable 设置 change 参数的优点
- 伸缩性:可以增加缓冲性,到达一定的阈值后再触发该方法
- 筛选性:并不是每次变更都会通知观察者,比如朋友圈的通知范围,只有 A 的相关朋友点赞才会通知 A
- 回滚性:事务执行失败,通知也应该取消掉时,调用 clearChanged 方法可以解决
- 控制权:setChanged 和 clearChanged 都是受保护的方法,外部无法调用,控制权在目标对象手中
观察者设计模式的推拉模型比较
- 推模型是假定目标对象知道观察者要观察的数据;而拉模型是假定目标表对象不知道观察者要观察哪些数据,传递自身,参数自取;
- 推模型会使观察者难以复用,如果需要观察其他数据,观察者必须重新定义新的或者修改已有的更新接口。
观察者设计模式的优缺点
优点
- 实现了目标对象和观察者之间的抽象耦合。目标对象只知道观察者接口,不知道具体的观察者类。
- 实现了动态联动。有了对观察者的动态管理,可以在运行期间,动态的控制注册的观察者,实现某个动作的联动管理。
- 支持广播通信。目标发送通知的范围可以是全部注册的观察者,也可以是限定范围的观察者,但要注意避免互为目标对象和观察者的两个对象间造成死循环的广播问题。
缺点
- 造成无谓的更新。有时候有些观察者不需要执行相应的更新处理,那就造成了浪费;如果本应该在执行通知前删除某些观察者,但忘记了,容易引起误操作。
- 循环通知观察者,当观察者数量较多时,效率低下,需要改造成异步通知
观察者设计模式和发布订阅模式的比较
- 观察者设计模式只有目标和观察者两个角色;发布订阅有发布,订阅和中间经纪人三个角色;
- 观察者模式目标对象和观察者存在抽象层存在松耦合关系;发布订阅模式不存在耦合关系;
- 观察者模式用于单应用内部;发布订阅用于跨应用场景。
JDK 9 废弃 JDK 1 观察者模式的原因
- Observable 没有实现 Serializable 接口,它的内部成员变量都是私有的,子类不能通过继承它来对 Observable 的成员变量处理,所以子类也不能序列化
- 子类可以覆写 Observable 的方法,事件通知可以以不同的顺序或者可能在不同的线程上发生
- 观察者更新方法抛出异常会导致整个通知过程失败,需手动 catch
- 抽象目标类持有的观察者对象列表性能察差,且扩容时浪费,Vector 的容量每次扩一倍,ArrayList 扩容只增加一半,CopyOnWriteArrayList 一个个增加
- 目标对象通知观察者的顺序是倒序,导致通知观察者的顺序和注册的顺序不一样,对有顺序要求的时候需增加额外处理
- Observable 抽象目标是类,违背了"组合优于继承"的设计原则,如果目标对象还想继承其他业务类就难以为继了
- 观察者无法感知目标发生了怎样的变化,仅仅只知道发生了变化,拿不到原来的值
- java.beans 提供了 PropertyChangeEvent 和 PropertyChangeListener 来代替目前 Observer 和 Observable
JDK 9 实现观察者模式
- 具体目标对象 Newspaper
Java
public class Newspaper {
private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);
/**
* 内容
*/
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
String oldValue = this.content;
this.content = content;
changeSupport.firePropertyChange("content", oldValue, this.content);
}
/**
* 注册观察者
*
* @param listener 观察者
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
/**
* 移除观察者
*
* @param listener 观察者
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
}
- 具体观察者 Reader
Java
public class Reader implements PropertyChangeListener {
private final String name;
public Reader(String name) {
this.name = name;
}
/**
* 对通知作相应处理
*
* @param evt 变更事件
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println(this.name + "上次阅读:" + evt.getOldValue() + ", 当前阅读:" + evt.getNewValue());
System.out.println(evt.getSource() instanceof Newspaper);
}
}
- 观察者测试
Java
public class Client {
public static void main(String[] args) {
Newspaper newspaper = new Newspaper();
newspaper.setContent("射雕英雄传");
newspaper.addPropertyChangeListener(new Reader("zhangsan"));
newspaper.addPropertyChangeListener(new Reader("lisi"));
newspaper.addPropertyChangeListener(new Reader("wangwu"));
newspaper.setContent("倚天屠龙记");
}
}