文章目录
前言
在学习发布订阅模式的时候,我在网上的教程中看到关于观察者模式和发布订阅模式的对比,二者同属于行为型模式。虽然它们在行为上存在相似性,但却是两种不同的设计模式。
再加上我自己在复习学校OOAD课程上讲过的GoF前面几种模式时,参考了菜鸟网(www.runoob.com)上关于设计模式的教程,看到里面有提到“观察者模式”(但是没有发布订阅模式),于是便好奇点进去看了一下,顺带学习了一下观察者模式。
后面,在看部分前端Vue面经的时候,了解到Vue框架就是基于发布订阅模式设计的。为了帮助自己理解发布订阅模式的思想以及应用于实际开发中,于是我便对Vue框架的源码进行了分析和学习,再次领悟其中的“发布订阅”思想。
一、引入——观察者模式
以下内容整理自菜鸟网(www.runoob.com)上的教程关于“观察者模式”的教程。
1.观察者模式的介绍
- 意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
- 主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
- 何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
- 如何解决:通过面向对象技术弱化这种依赖关系。
- 关键代码:在抽象类里有一个 ArrayList 存放观察者们。
- 在实际场景中的应用举例:在拍卖会中,拍卖师观察最高标价,然后通知给其他竞价者竞价。
2.使用观察者模式的场景:
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
- 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
- 一个对象必须通知其他对象,而并不知道这些对象是谁。即达到广播效果。
- 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
3.举例展示观察者模式的实现
前面列举的干货都是通过提炼出来的,可能不太便于理解,那就通过编写一个小小的java程序demo来举例帮助理解吧~
本例子中,创建了Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类(BinaryObserver、OctalObserver、HexaObserver)来实现观察者模式。使用ObserverPatternDemo演示类来使用 Subject 和实体类对象来演示观察者模式。
(1)UML类图 、顺序图展示如下:
![](https://img-blog.csdnimg.cn/img_convert/748af95c39766d83f5ef4716288a8d30.png)
![](https://img-blog.csdnimg.cn/img_convert/75e54cb2059e8e4e5d3bdc3bfc27185d.png)
(2) 代码实现:
步骤 1
创建 Subject 类。
import java.util.ArrayList;
import java.util.List;
public class Subject {
//定义一个用于存放观察者的数组
private List<Observer> observers
= new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public void attach(Observer observer){
observers.add(observer);
}
public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}
步骤 2
创建 抽象Observer 类。
//Observer:抽象类
public abstract class Observer {
protected Subject subject;
public abstract void update();
}
步骤 3
创建实体观察者类(BinaryObserver、OctalObserver、HexaObserver),重写update方法。
//BinaryObserver实体类,用于将十进制转化为二进制
public class BinaryObserver extends Observer{
public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Binary String: "
+ Integer.toBinaryString( subject.getState() ) );
}
}
//OctalObserver实体类,用于将十进制转化为八进制
public class OctalObserver extends Observer{
public OctalObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Octal String: "
+ Integer.toOctalString( subject.getState() ) );
}
}
//HexaObserver实体类,用于将十进制转化为十六进制
public class HexaObserver extends Observer{
public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Hex String: "
+ Integer.toHexString( subject.getState() ).toUpperCase() );
}
}
步骤 4
在ObserverPatternDemo演示类中使用 Subject 和实体观察者对象。
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new HexaObserver(subject);
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}
步骤 5
执行程序,输出结果:
4.对观察者模式的理解
观察者模式适用于当一个对象的数据更新时,这个对象需要让其他对象也各自更新自己的数据,但这个对象不知道具体有多少对象需要更新数据的情况。
观察者模式包含以下三种角色:
Subject(主题/具体主题):可以是接口,后续再由实现类来实现该主题接口;
也可以直接定义为实现类,表示具体的主题。
Observer(观察者):接口,规定了具体观察者更新数据的方法。
ConcreteObserver(具体观察者):实现观察者接口类的实例。
当Subject(具体主题)内的数据发生改变时,由于Subject内存在存放Observer(观察者)的集合,比如ArrayList,因此Subject内数据变化时就能及时地通知到Observer,Observer对应的ConcreteObserver(具体观察者)就能对数据变化作出不同的响应。实现一个通知,触发多个事件的效果。
- 观察者模式的优点:
(1)具体主题和具体观察者间是松耦合关系。因为主题只依赖观察者接口、具体观察者泛化了观察者接口,因此具体主题和具体观察者间不需要互相知道类的信息。
(2)满足“开闭原则”。主题接口/具体主题仅依赖观察者接口,若要新增观察者接口,不需要修改主题。
- 缺点:
(1)若一个被观察者有许多直接、间接观察者,则需要花费大量时间让所有观察者接收到通知。
(2)观察者模式没有相应机制让观察者知道观察的目标如何发生变化,仅知道观察目标发生了变化。
二、发布订阅模式
观察者模式中被观察者和观察者之间实现的是松耦合,那么有没有完全解耦的类似模式呢?答案是有的,而发表订阅模式就实现了发布者和订阅者之间的完全解耦。
观察者模式存在一些弊端,比如说观察者模式提供给关联对象的是一种同步通信的手段,就会存在需要花时间等待传输完成的问题。而发布订阅模式大多数时候采用的是使用消息队列(中间件)实现异步并发。
下面来对发布订阅模式进行介绍。
1.发布订阅模式的介绍
发布订阅模式与观察者模式的定义其实差不多,
唯一的不同点就在于:
订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
通俗地来说,就是在发布订阅模式中,发布方向订阅方广播发送信息是通过中间件来实现的;而在观察者模式中,被观察者与观察者之间是直接广播信息的。
发布订阅模式在实际场景中的举例:
比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。
在此场景中,公众号属于发布者(Publisher),用户属于订阅者(Subscriber);用户将订阅公众号的事件注册到调度中心(Channel),公众号作为发布者,当有新文章(Message)发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户新文章发布。
2.发布订阅模式的优、缺点
优点:
(1)松耦合/Independence:
可以将众多需要通信的子系统(Subsystem)解耦,每个子系统都可以独立管理。而且即使部分子系统下线了,也不会影响系统消息的整体管理。
同时,为应用程序提供了关注点分离。每个应用程序都可以专注于其核心功能,而消息传递基础结构负责将消息路由到每个消费者手里。
(2)高伸缩性/Scalability:
发送方向中间件发送信息后就能返回其核心处理业务,不用关注何时信息全部成功发送给订阅方;由中间件负责把信息传递到每一个订阅方手中。
(3)高可靠性/Reliability:
异步传输的功劳,有助于应用程序在增加的负载下继续平稳运行,并且可以更有效地处理间歇性故障。
(4)灵活性/Flexibility:
不用关心组件的组合原理,只需他们遵守同一份协议即可。
(5)可测试性/Testability:
中间件可以被监视,从而降低了测试难度;发送的消息可以作为整体集成测试策略的一部分而被检查或记录。
缺点:
由于增加了中间件的代理,为系统增加了复杂度,发布方不能实时指导自己的发表的信息是否已被订阅方收到,增加了系统的不确定性。
3.使用发布订阅模式的场景
(1)应用程序需要向大量消费者广播信息,例如微信订阅号。
(2)应用程序需要与一个或多个独立开发的应用程序或服务通信,这些应用程序或服务可能使用不同的平台、编程语言和通信协议。
例如:现在很多的互联网产品同时拥有网页端、pc端、移动端app、小程序等多种形态,当程序员对产品中的某个功能进行了修复,那么应该各个平台的bug都要进行修复,为了避免重复劳动就可以使用发布订阅模式,通过中间件将修改广播到各个view层。
(3)应用程序可以向消费者发送信息,而不需要消费者的实时响应。或者,消费者可能具有与发送者不同的可用性要求或正常运行时间计划。例如你消息在上午发布了出去,消费者计划在下午才去处理这些消息。
(4)被集成的系统被设计为支持其数据的最终一致性模型。
4.发布订阅模式的实现举例
关键部分类图如下:
关键部分顺序图如下:
步骤 1
定义发布者接口:所有的发布者都要实现该接口,该接口定义添加订阅者、发送消息、移除订阅者方法。
public interface IPublisher {
/**
* 添加订阅者
*/
void emit(ISubscriber subscriber);
/**
* 触发消息
*/
void on(String msg);
/**
* 移除订阅者
*/
void remove(ISubscriber subscriber);
}
步骤 2
创建发布者实现类:这里以EventBus为例,实现发布者接口,并在该类中创建一个集合来添加存储订阅者。当发布消息的时候遍历该集合,调用订阅者的通知函数,将消息发送出去。
public class EventBus implements IPublisher {
// 定义一个集合来存储订阅者
private ArrayList events = new ArrayList<Subscriber>();
// 发布者名称
private String name;
// 创建发布者
public EventBus(String name) {
this.name = name;
}
@Override
public void emit(ISubscriber subscriber) {
// 将每一个新的订阅者添加到集合中维护
events.add(subscriber);
}
@Override
public void on(String msg) {
// 触发订阅的原理就是遍历该集合,然后将消息发送给集合中的每一个订阅者
for (int i = 0; i < events.size(); i++) {
Subscriber subscriber = (Subscriber) events.get(i);
subscriber.on(this.name, msg);
}
}
@Override
public void remove(ISubscriber subscriber) {
// 移除订阅者
events.remove(subscriber);
}
}
步骤 3
定义订阅者接口:接口定义收到消息监听方法,所有的订阅者都实现该接口。
public interface ISubscriber {
void on(String publisher, String msg);
}
步骤 4
创建订阅者实现类:实现订阅者接口。
public class Subscriber implements ISubscriber {
// 订阅者名称
private String name;
private Receiver receiver;
public interface Receiver {
void onMessage(String publisher, String msg);
}
// 创建订阅者
public Subscriber(String name) {
this.name = name;
}
// 创建订阅者,也可以加个回调函数,用来将消息回调出去
public Subscriber(String name, Receiver receiver) {
this.name = name;
this.receiver = receiver;
}
@Override
public void on(String publisher, String msg) {
// 这里会收到发布者消息,然后做相应的处理,一般这里会创建一个回调函数,通过回调函数将消息发送出去
System.out.println(publisher + "发布了新消息;" + this.name + ",收到消息通知:" + msg);
if (receiver != null) {
receiver.onMessage(publisher, msg);
}
}
}
步骤 5
使用示例:
Subscriber subscriber1 = new Subscriber("张三", new Subscriber.Receiver() {
@Override
public void onMessage(String publisher, String msg) {
// 收到消息
System.out.println(publisher + ":" + msg);
}
}); // 创建订阅者张三
Subscriber subscriber2 = new Subscriber("李四"); // 创建订阅者李四
Subscriber subscriber3 = new Subscriber("王五"); // 创建订阅者王五
EventBus eventBus = new EventBus("小明"); // 创建发布者小明
eventBus.emit(subscriber1); // 添加张三订阅者
eventBus.emit(subscriber2); // 添加李四订阅者
eventBus.emit(subscriber3); // 添加王五订阅者
eventBus.on("祝你虎年大吉大利"); // 发布新消息
示例结果:
小明发布了新消息;张三,收到消息通知:祝你虎年大吉大利
小明:祝你虎年大吉大利
小明发布了新消息;李四,收到消息通知:祝你虎年大吉大利
小明发布了新消息;王五,收到消息通知:祝你虎年大吉大利
三、结合Vue框架进行分析
在学习发布订阅模式的相关思想的时候,我就联想到vue中的父子组件通信和双向数据绑定,好奇它在实现的时候是不是也应用到了“发布订阅”的模式。因此,我就去网上搜了vue框架的源码以及别人研究vue源码的教程来学习了一下。
Vue项目目录下的核心文件“src”下的文件目录如下:
├─ src // 主要源码所在位置,核心内容
│ ├─ compiler // 模板编译相关文件,将 template 编译为 render 函数
│ ├─ codegen // 把AST(抽象语法树)转换为Render函数
│ ├─ directives // 生成Render函数之前需要处理的东西
│ ├─ parser // 模板编译成AST
│ ├─ core // Vue核心代码,包括了内置组件、全局API封装、Vue实例化、响应式原理、vdom(虚拟DOM)、工具函数等等。
│ ├─ components // 组件相关属性,包含抽象出来的通用组件 如:Keep-Alive
│ ├─ global-api // Vue全局API,如Vue.use(),Vue.nextTick(),Vue.config()等,包含给Vue构造函数挂载全局方法(静态方法)或属性的代码。 链接:https://012-cn.vuejs.org/api/global-api.html
│ ├─ instance // 实例化相关内容,生命周期、事件等,包含Vue构造函数设计相关的代码
│ ├─ observer // 响应式核心目录,双向数据绑定相关文件。包含数据观测的核心代码
│ ├─ util // 工具方法
│ └─ vdom // 虚拟DOM相关的代码,包含虚拟DOM创建(creation)和打补丁(patching)的代码
│ ├─ platforms // vue.js和平台构建有关的内容 不同平台的不同构建的入口文件也在这里 (Vue.js 是一个跨平台的MVVM框架)
│ ├── web // web端 (渲染,编译,运行时等,包括部分服务端渲染)
│ │ ├── compiler // web端编译相关代码,用来编译模版成render函数basic.js
│ │ ├── entry-compiler.js // vue-template-compiler 包的入口文件
│ │ ├── entry-runtime-with-compiler.js // 独立构建版本的入口,它在 entry-runtime 的基础上添加了模板(template)到render函数的编译器
│ │ ├── entry-runtime.js // 运行时构建的入口,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。
│ │ ├── entry-server-basic-renderer.js // 输出 packages/vue-server-renderer/basic.js 文件
│ │ ├── entry-server-renderer.js // vue-server-renderer 包的入口文件
│ │ ├── runtime // web端运行时相关代码,用于创建Vue实例等
│ │ ├── server // 服务端渲染(ssr)
│ │ └── util // 工具类相关内容
│ └─ weex // 混合运用 weex框架 (一端开发,三端运行: Android、iOS 和 Web 应用) 2016年9月3日~4日 尤雨溪正式宣布以技术顾问的身份加盟阿里巴巴Weex团队, 做Vue和Weex的整合 让Vue的语法能跨三端
│ ├─ server // 服务端渲染相关内容(ssr)
│ ├─ sfc // 转换单文件组件(*.vue)
│ └─ shared // 共享代码 全局共享的方法和常量
看到“observer”的时候,我就猜想这个模块是不是就是实现了“发布订阅模式”的呢?自己分析了下源码,然后去看了下教程:
observer模块共分为这几个部分:
- Observer: 数据的观察者,让数据对象的读写操作都处于自己的监管之下
- Watcher: 数据的订阅者,数据的变化会通知到Watcher,然后由Watcher进行相应的操作,例如更新视图
- Dep: Observer与Watcher的纽带,当数据变化时,会被Observer观察到,然后由Dep通知到Watcher。
示意图如下:
这样看来,其中的Dep不是正与“发布订阅模式”中的中间件所起的作用一样吗?Observer相当于发布方,Watcher相当于订阅方。因此,我认为observer模块实现了“发布订阅模式”。
源码分析
(1)Observer
Observer类定义在src/core/observer/index.js中,以下是Observer的构造函数:
constructor (value: any) {
//value:需要被观察的数据对象
this.value = value
this.dep = new Dep()
this.vmCount = 0
//给value加上__ob__属性,作为数据已被Observer观察的标志
def(value, '__ob__', this)
//判断value是否为数组
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
//value为数组:用observeArray遍历value,对value中每一个元素调用observer进行观察
this.observeArray(value)
}
//value不是数组:walk遍历value上每个key,每个key的数据调用defineReactive来获得该key的set/get控制权
else {
this.walk(value)
}
}
(2)Dep
Dep是Observer与Watcher之间的纽带,Watcher订阅某个Observer的Dep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher。
// Dep是订阅者Watcher对应的数据依赖
var Dep = function Dep () {
//每个Dep都有唯一的ID
this.id = uid++;
//subs用于存放依赖
this.subs = [];
};
//向subs数组添加依赖
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
//移除依赖
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
//设置某个Watcher的依赖
//这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
//也就是说判断他是Watcher的this.get调用的,而不是普通调用
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
//通知所有绑定 Watcher。调用watcher的update()
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
此外,Dep中还提供了几个接口:
- addSub: 接收的参数为Watcher实例,并把Watcher实例存入记录依赖的数组中
- removeSub: 与addSub对应,作用是将Watcher实例从记录依赖的数组中移除
- depend: Dep.target上存放这当前需要操作的Watcher实例,调用depend会调用该Watcher实例的addDep方法,addDep的功能可以看下面对Watcher的介绍
- notify: 通知依赖数组中所有的watcher进行更新操作
(3)Watcher
Watcher是用来订阅数据的变化的并执行相应操作(例如更新视图)的。以下是Watcher的构造器函数:
constructor (vm, expOrFn, cb, options) {
//vm:组件实例
this.vm = vm
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
//cb:watcher运行后的回调函数
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
//expOrFn:要订阅的数据字段;或是一个要执行的函数
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
watcher实例上有这些方法:
- get: 将Dep.target设置为当前watcher实例,在内部调用this.getter,如果此时某个被Observer观察的数据对象被取值了,那么当前watcher实例将会自动订阅数据对象的Dep实例
- addDep: 接收参数dep(Dep实例),让当前watcher订阅dep
- cleanupDeps: 清除newDepIds和newDep上记录的对dep的订阅信息
- update: 立刻运行watcher或者将watcher加入队列中等待统一flush
- run: 运行watcher,调用this.get()求值,然后触发回调
- evaluate: 调用this.get()求值
- depend: 遍历this.deps,让当前watcher实例订阅所有dep
- teardown: 去除当前watcher实例所有的订阅
四、小结
以上就是本人对发布订阅模式的一篇学习笔记,本人认为,发布订阅模式主要用于解决了这两个问题:
发布方如何将新信息推送给所有订阅方;发布方和订阅方的完全解耦的异步并发。
订阅方不用再去频繁地查看发布方到底发了新信息没有;而发布方将新的信息传送到中间件,让中间件帮助完成广播给所有订阅方的工作,可见发布方和订阅方之间没有直接的关联,实现了解耦。因此发布方可以更加专注于自己的本身业务,中间件也可以对广播的时间进行自己的调整,体现了异步的思想。