设计模式提供了软件开发过程中的一些最佳实践,可以帮助我们解决常见的编程问题,提高软件的可维护性和可复用性,并使我们的代码更加健壮和灵活。设计模式可以带来以下好处:提高代码的可读性和可维护性、提高软件的可复用性、提高开发效率、提高系统的灵活性和可扩展性。今天我们讲一下观察者模式的具体应用。
观察者模式是一种软件设计模式,它允许一个对象(称为“主题”)管理其依赖项(称为“观察者”),它定义了对象之间的一对多依赖关系,当一个对象状态发生改变时,其相关依赖项将会自动收到通知。这种模式提供了一种灵活的方式,将一个对象的状态与依赖它的多个对象联系起来。
在观察者模式中,主题和观察者之间建立了一种订阅关系。主题负责维护其状态并提供一个注册表,用于存储与其相关联的观察者对象。当主题的状态发生改变时,它会自动通知所有与之相关联的观察者,并传递相应的参数。观察者接收到通知后,可以执行相应的操作来响应主题状态的改变。
意图
观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。
问题
假如你有两种类型的对象: 顾客
和 商店
。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。
顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。
前往商店和发送垃圾邮件
另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。
我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。
拥有一些值得关注的状态的对象通常被称为目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。
观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 1) 一个用于存储订阅者对象引用的列表成员变量; 2) 几个用于添加或删除该列表中订阅者的公有方法。
订阅机制允许对象订阅事件通知。
现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。
实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。
因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。
发布者调用订阅者对象中的特定通知方法来通知订阅者。
如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有发布者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。
杂志和报纸订阅。
如果你订阅了一份杂志或报纸, 那就不需要再去报摊查询新出版的刊物了。 出版社 (即应用中的 “发布者”) 会在刊物出版后 (甚至提前) 直接将最新一期寄送至你的邮箱中。
出版社负责维护订阅者列表, 了解订阅者对哪些刊物感兴趣。 当订阅者希望出版社停止寄送新一期的杂志时, 他们可随时从该列表中退出。
我们看一段代码示例,然后再通过示例进行分析。在JavaScript中,我们可以使用原型或类来实现观察者模式。下面是一个使用原型的实现示例:
// 观察者接口
var Observer = function() {};
Observer.prototype.update = function(data) {};
// 具体观察者
var ConcreteObserver1 = function() {};
ConcreteObserver1.prototype = Object.create(Observer.prototype);
ConcreteObserver1.prototype.constructor = ConcreteObserver1;
ConcreteObserver1.prototype.update = function(data) {
console.log('ConcreteObserver1 received data: ' + data);
};
// 具体观察者
var ConcreteObserver2 = function() {};
ConcreteObserver2.prototype = Object.create(Observer.prototype);
ConcreteObserver2.prototype.constructor = ConcreteObserver2;
ConcreteObserver2.prototype.update = function(data) {
console.log('ConcreteObserver2 received data: ' + data);
};
// 主题
var Subject = function() {
this.observers = [];
};
Subject.prototype.registerObserver = function(observer) {
this.observers.push(observer);
};
Subject.prototype.notifyObservers = function(data) {
for (var i = 0; i < this.observers.length; i++) {
this.observers[i].update(data);
}
};
// 具体主题
var ConcreteSubject = function() {};
ConcreteSubject.prototype = Object.create(Subject.prototype);
ConcreteSubject.prototype.constructor = ConcreteSubject;
ConcreteSubject.prototype.setState = function(data) {
this.notifyObservers(data);
};
在上面的代码中,我们首先定义了一个Observer接口,它包含一个update方法。然后我们创建了两个具体的观察者ConcreteObserver1和ConcreteObserver2,它们都实现了Observer接口的update方法。接着我们定义了一个主题Subject,它包含一个观察者数组和一个注册方法registerObserver,以及一个通知方法notifyObservers。最后我们创建了一个具体主题ConcreteSubject,它继承了Subject的原型并实现了一个setState方法,该方法调用通知方法来通知所有观察者状态改变。
在上面的示例中,我们使用了原型继承来实现Observer接口和具体的观察者类。在实际应用中,我们也可以使用类继承或ES6的class语法来实现这些类。另外,在具体主题ConcreteSubject中,我们通过调用notifyObservers方法来通知所有观察者状态改变,这个方法可以传递一个参数作为通知的内容。在具体观察者的update方法中,我们可以根据传递的参数来执行相应的操作。
除了使用JavaScript实现观察者模式外,我们还可以在其他编程语言和框架中找到这种模式的实现。例如,Redis的订阅模型和WebSocket请求都使用了类似的方式来实现主题和观察者之间的订阅关系。这些实现方式都允许客户端订阅特定主题,并在主题状态发生改变时自动接收通知。
观察者模式适合应用场景
当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。
当你使用图形用户界面类时通常会遇到一个问题。 比如, 你创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。
观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。
当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用。
订阅列表是动态的, 因此订阅者可随时加入或离开该列表。
- 仔细检查你的业务逻辑, 试着将其拆分为两个部分: 独立于其他代码的核心功能将作为发布者; 其他代码则将转化为一组订阅类。
- 声明订阅者接口。 该接口至少应声明一个
update
方法。 - 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象。 记住发布者必须仅通过订阅者接口与它们进行交互。
- 确定存放实际订阅列表的位置并实现订阅方法。 通常所有类型的发布者代码看上去都一样, 因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的。 具体发布者会扩展该类从而继承所有的订阅行为。
但是, 如果你需要在现有的类层次结构中应用该模式, 则可以考虑使用组合的方式: 将订阅逻辑放入一个独立的对象, 然后让所有实际订阅者使用该对象。 - 创建具体发布者类。 每次发布者发生了重要事件时都必须通知所有的订阅者。
- 在具体订阅者类中实现通知更新的方法。 绝大部分订阅者需要一些与事件相关的上下文数据。 这些数据可作为通知方法的参数来传递。
但还有另一种选择。 订阅者接收到通知后直接从通知中获取所有数据。 在这种情况下, 发布者必须通过更新方法将自身传递出去。 另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来。 - 客户端必须生成所需的全部订阅者, 并在相应的发布者处完成注册工作。
- 降低目标与观察者之间的耦合关系
- 支持“广播通信”
- 符合开闭原则。
通知可能会花费很长时间
循环依赖的问题
没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的
总结一下,观察者模式适用于任何需要实现一对多依赖关系的场景,使得主题的状态改变可以自动通知给所有的观察者。
- 订阅/发布系统:观察者模式可以用于实现订阅/发布系统。主题可以代表各种事件或消息,观察者可以订阅感兴趣的主题并接收相关的通知。
- 实时通信:观察者模式可以用于实现实时通信。当某个事件发生时,相关的观察者可以立即得到通知并做出相应的响应。
- 数据绑定:在图形用户界面开发中,观察者模式可以用于实现数据绑定。当某个数据源发生改变时,相关的视图可以自动更新。
- 事件驱动系统:观察者模式可以用于实现事件驱动系统。当某个事件触发时,相关的观察者可以收到通知并执行相应的操作。
- 异步消息处理:在分布式系统中,观察者模式可以用于实现异步消息处理。当某个消息到达时,相关的观察者可以收到通知并处理该消息。