🔥 核心
观察者模式即发布-订阅模式。
发布者的状态发生改变,所有订阅者都将得到通知。
🙁 问题场景
小镇里开了一家苹果专卖店,你是这家店的店主。
一方面,小镇里的许多顾客都对苹果的电子产品十分感兴趣,他们每天都会来店里,瞧瞧有没有新发布的产品到货。当然,他们总是空手而归。
另一方面,每次有新发布的产品到货,你都会发邮件给小镇里所有的居民。但有的居民对苹果的电子产品丝毫不感兴趣,收到邮件后直接将其视为垃圾邮件,丢进邮件垃圾箱。
你似乎遇到了一个矛盾:要么让顾客浪费时间到店里来确认产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。
🙂 解决方案
你想到了一个好法子:维护一个“对苹果电子产品感兴趣的顾客”的列表,这部分顾客会向你注册上名字(订阅),也可以请求删掉自己的名字(退订),而你只会发送邮件给名字在列表上的顾客。
这就是「发布-订阅模式」。
再举一个例子,在这个例子的语境下,「观察者模式」这个名字会更加贴切。有一个时尚博主,她经常在社交平台上发布自己的穿搭,许多女孩时刻“观察”着她,只要有新动态,女孩们就会竞相模仿。仔细体会一下,「发布-订阅模式」和「观察者模式」完全是一种东西嘛~
🌈 有趣的例子
有几个人正在做一个无聊的游戏。
其中一个人举着 数字牌(NumberCard)
,并可以随时修改数字牌上的 数字(num)
。其他人(BinObserver、OctObserver、HexObserver)
观察着数字牌,每当数字牌上的数字变化时,他们分别说出新数字的二进制/八进制/十六进制表示。
观察者(接口)
interface Observer {
void update(int num);
}
二进制观察者
class BinObserver implements Observer {
@Override
public void update(int num) {
System.out.println("Bin string: " + Integer.toBinaryString(num));
}
}
八进制观察者
class OctObserver implements Observer {
@Override
public void update(int num) {
System.out.println("Oct string: " + Integer.toOctalString(num));
}
}
十六进制观察者
class HexObserver implements Observer {
@Override
public void update(int num) {
System.out.println("Hex string: " + Integer.toHexString(num));
}
}
数字牌(被观察者)
class NumberCard {
// 数字
private int num;
// 观察者列表
private List<Observer> observerList = new ArrayList<>();
// 设置数字的同时,通知观察者们
public void setNum(int num) {
this.num = num;
notifyObservers();
}
// 向观察者列表中添加观察者
public void attachObserver(Observer observer) {
observerList.add(observer);
}
// 通知观察者列表中的所有观察者
public void notifyObservers() {
for (Observer observer : observerList) {
observer.update(num);
}
}
}
public class ObserverPatternDemo {
public static void main(String[] args) {
// 创建二/八/十六进制观察者
Observer binObserver = new BinObserver();
Observer octObserver = new OctObserver();
Observer hexObserver = new HexObserver();
// 创建数字牌(被观察者)
NumberCard numberCard = new NumberCard();
// 注册观察者
numberCard.attachObserver(binObserver);
numberCard.attachObserver(octObserver);
numberCard.attachObserver(hexObserver);
// 给被观察者设置数字的同时,所有观察者都会得到通知
numberCard.setNum(10);
numberCard.setNum(8080);
}
}
Bin string: 1010
Oct string: 12
Hex string: a
Bin string: 1111110010000
Oct string: 17620
Hex string: 1f90
☘️ 使用场景
◾️当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用观察者模式。
当你使用图形用户界面类时通常会遇到一个问题。比如,你创建了自定义按钮类并允许客户端在按钮中注入自定义代码,这样当用户按下按钮时就会触发这些代码。
观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。你可在按钮中添加订阅机制,允许客户端通过自定义订阅类注入自定义代码。
◾️当应用中的一些对象必须观察其他对象时,可使用该模式。但仅能在有限时间内或特定情况下使用。
订阅列表是动态的,因此订阅者可随时加入或离开该列表。
🧊 实现方式
(1)仔细检查你的业务逻辑,试着将其拆分为两个部分:独立于其他代码的核心功能将作为发布者;其他代码则将转化为一组订阅类。
(2)声明订阅者接口。该接口至少应声明一个 update 方法。
(3)声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。记住发布者必须仅通过订阅者接口与它们进行交互。
(4)确定存放实际订阅列表的位置并实现订阅方法。通常所有类型的发布者代码看上去都一样,因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。具体发布者会扩展该类从而继承所有的订阅行为。
(但是,如果你需要在现有的类层次结构中应用该模式,则可以考虑使用组合的方式:将订阅逻辑放入一个独立的对象,然后让所有实际订阅者使用该对象。)
(5)创建具体发布者类。每次发布者发生了重要事件时都必须通知所有的订阅者。
(6)在具体订阅者类中实现通知更新的方法。绝大部分订阅者需要一些与事件相关的上下文数据。这些数据可作为通知方法的参数来传递。
(但还有另一种选择。订阅者接收到通知后直接从通知中获取所有数据。在这种情况下,发布者必须通过更新方法将自身传递出去。另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。)
(7)客户端必须生成所需的全部订阅者,并在相应的发布者处完成注册工作。
🎲 优缺点
➕ 你可以在运行时建立对象之间的联系。
➕ 观察者和被观察者是抽象耦合的。
➕ 开闭原则。你无需修改发布者代码就能引入新的订阅者类(如果是发布者接口则可轻松引入发布者类)。
➖ 订阅者的通知顺序是随机的。
➖ 观察者和被观察者存在的循环依赖,会导致系统崩溃。
🌸 补充
许多组件都是观察者模式/发布订阅模式的具体实现,比如 Zookepper 。