观察者(Observer)模式,是一种行为型设计模式,允许你定义一种订阅机制,可以在对象事件发生时通知更多个“观察”该对象的其他对象,类似于“订阅—通知”
问题
假如你有两种类型的对象,顾客
和商店
。其中一些顾客对某个特定的手机感兴趣,比如华为、苹果等等,而这些产品很快就会在你的店内出售。
顾客每天来商店看产品是否到货。但如果商品尚未到货时,绝大多数的顾客都会空手而归。
另一方面,当产品到货时,商店可以向所有顾客发送邮件(有可能被视为垃圾邮件)。这样,部分顾客就无需反复前往商店了,但也可能惹恼那些不感兴趣的其他顾客。
我们似乎遇到一个问题:要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。
解决方案
根据我们的结构来看,我们可以定义一个发布者和一个订阅者,那么合理的顾客就是订阅者,而我们的商店自然就是发布者。
发布者是我们,所以我们只需要定义订阅者,同时因为订阅者只需要接收信息就行,但是不同的用户可以接收不同的信息,我们就抽象为接口:
- Observer
public interface Observer {
/**
* 更新信息
* @param type 信息类型
* @param number 信息内容
*/
public void update(String type,String number);
}
但是我们需要获取数据来发送通知,这个通知我们称为订阅信息,那他们的关系自然就是商店—>信息—>订阅者。但是信息可以是多样的(比如华为手机到了,或者苹果手机到了),我们只需要展示是什么信息就可以。不同的信息是不同的顾客订阅的,所以我们将“信息“单独抽象出来:
- Subject类,作为信息的载体
public interface Subject {
/**
* 用户需要订阅,所以订阅的用户作为对象作为参数传入
* @param type 类型
* @param o 订阅者载体
*/
public void registerObserver(String type,Observer o);
/**
* 用户哪天不需要订阅了,我们可以给他删除
* @param type 类型
* @param o 订阅者载体
*/
public void removeObserver(String type,Observer o);
/**
* @param type 类型
* 更新时我们通知所有订阅者(订阅该信息的),所以不需要参数
*/
public void notifyObserver(String type);
}
当然我们既然发送了,根据不同的人我们需要展示出来,所以还展示的接口:
- DisplayElement接口
public interface DisplayElement {
/**
* 发布显示的信息
*/
public void display();
}
好了我们现在大致框架已经搭出来了,那么在订阅者层次怎么实现呢?我们拿需求为苹果手机的顾客来说:
- 对于商店,他是订阅者中的其中一个
- 他需要加入商店的订阅列表
- 需要发送iPhone的消息给他
那么我们是不是先需要构建消息给他呢?
- MessageData
/**
* 信息载体实现Subject接口,实现其中的方法
*/
public class MessageData implements Subject {
/**
* 订阅该消息的顾客列表
*/
private final Map<String,List<Observer>> observers;
/**
* 类型
*/
private String type;
/**
* 数量
*/
private String number;
public MessageData(){
observers = new HashMap<>();
}
@Override
public void registerObserver(String type,Observer o) {
//新用户订阅
observers.computeIfAbsent(type,t -> new ArrayList<>()).add(o);
}
@Override
public void removeObserver(String type,Observer o) {
//用户取消订阅
int index = observers.get(type).indexOf(o);
if(index > 0){
observers.get(type).remove(o);
}
}
@Override
public void notifyObserver(String type) {
for (Observer observer : observers.get(type)) {
//如果有新消息,通知已经订阅的用户
observer.update(type,number);
}
}
/**
* 当我们获取到新消息时,通知观察者
*/
public void messageChange(String brand){
notifyObserver(brand);
}
public void setMessage(String brand,String type,String number){
this.type = type;
this.number = number;
//更新消息
messageChange(brand);
}
//....其他方法
}
信息载体做完,就到了我们最期待的部分:订阅的用户
- IPhoneCustomers
//继承Observer接口和展示接口
public class IPhoneCustomers implements Observer,DisplayElement{
private final static String BRAND = "iPhone";
private String type;
private String number;
private Subject iPhoneData;
public IPhoneCustomers(Subject iPhoneData){
this.iPhoneData = iPhoneData;
//将自己注册进入订阅者列表
iPhoneData.registerObserver(BRAND,this);
}
//支持自定义信息
@Override
public void update(String type, String number) {
this.type = type;
this.number = number;
//到货后更新数据
display();
}
//更新后的信息展示
@Override
public void display() {
System.out.println("尊敬的"+BRAND+"用户您好,您喜欢的:"+type+"已经到货:"+number+"台,期待您的光临");
}
}
一切准备好之后,就开始启动我们的项目吧!
public class Main {
public static void main(String[] args) {
//苹果信息
MessageData messageData = new MessageData();
//苹果用户订阅
IPhoneCustomers iPhoneCustomers = new IPhoneCustomers(messageData);
messageData.setMessage("iPhone","iPhone13","40");
messageData.setMessage("iPhone","iPhone14 pro","30");
}
}
运行结果:
尊敬的iPhone用户您好,您喜欢的:iPhone已经到货:40台,期待您的光临
尊敬的iPhone用户您好,您喜欢的:iPhone已经到货:30台,期待您的光临
只有一个苹果用户没有问题,那么我们再加入一个华为的订阅者呢?
System.out.println("--------------split line--------------------");
HuaweiCustomers huaweiCustomers = new HuaweiCustomers(messageData);
messageData.setMessage("Huawei","华为13 pro max 1TB 冷锋蓝","200");
messageData.setMessage("iPhone","iPhone13 pro max","60");
运行结果:
--------------split line--------------------
尊敬的Huawei用户您好,您喜欢的:Huawei已经到货:200台,期待您的光临
尊敬的iPhone用户您好,您喜欢的:iPhone已经到货:60台,期待您的光临
可见两个不同的用户也没问题(HuaweiCustomers
类和IPhoneCustomers
类似,只是把brand改了)
那么单个没问题,多个相同的订阅者呢?
public class Main {
public static void main(String[] args) {
//苹果信息
MessageData messageData = new MessageData();
//苹果用户订阅
IPhoneCustomers iPhoneCustomers = new IPhoneCustomers(messageData);
messageData.setMessage("iPhone","iPhone13","40");
messageData.setMessage("iPhone","iPhone14 pro","30");
System.out.println("--------------split line--------------------");
//多个相同订阅者
HuaweiCustomers huaweiCustomers = new HuaweiCustomers(messageData);
HuaweiCustomers huaweiCustomers1 = new HuaweiCustomers(messageData);
messageData.setMessage("Huawei","华为13 pro max 1TB 冷锋蓝","200");
messageData.setMessage("iPhone","iPhone13 pro max","60");
}
}
运行结果:
尊敬的iPhone用户您好,您喜欢的:iPhone已经到货:40台,期待您的光临
尊敬的iPhone用户您好,您喜欢的:iPhone已经到货:30台,期待您的光临
--------------split line--------------------
尊敬的Huawei用户您好,您喜欢的:Huawei已经到货:200台,期待您的光临
尊敬的Huawei用户您好,您喜欢的:Huawei已经到货:200台,期待您的光临
尊敬的iPhone用户您好,您喜欢的:iPhone已经到货:60台,期待您的光临
没问题!
观察者模式结构
- 发布者 (Publisher) 会向其他对象发送值得关注的事件。 事件会在发布者自身状态改变或执行特定行为后发生。 发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
- 当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者对象的通知方法。 该方法是在订阅者接口中声明的。
- 订阅者 (Subscriber) 接口声明了通知接口。 在绝大多数情况下, 该接口仅包含一个
update
更新方法。 该方法可以拥有多个参数, 使发布者能在更新时传递事件的详细信息。 - 具体订阅者 (Concrete Subscribers) 可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。
- 订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传递。 发布者也可将自身作为参数进行传递, 使订阅者直接获取所需的数据。
观察者模式适合应用场景
🐞当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。
💡 当你使用图形用户界面类时通常会遇到一个问题。 比如, 你创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。
观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。
🐞当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用。
💡 订阅列表是动态的, 因此订阅者可随时加入或离开该列表。
实现方式
-
仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。
-
声明订阅者接口。 该接口至少应声明一个
update
方法。 -
声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。
-
确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。
但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。
-
创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。
-
在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。
但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下, 发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。
-
客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。
观察者模式优缺点
优点:
- 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
- 你可以在运行时建立对象之间的联系。
缺点:
- 订阅者的通知顺序是随机的。
与其他模式的关系
-
责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
-
中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。
中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。
有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。
当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。
假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。
参考:First Head设计模式、[设计模式](