设计模式手册之订阅-发布模式
官方定义:
订阅-发布模式:定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都可以得到通知。
我的想法:
将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相应对象间的一致性,这样会给维护、扩展和重用都带来不便。当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象需要改变时,就可以使用订阅发布模式了。一个抽象模型有两个方面,其中一方面依赖于另一方面,这时订阅发布模式可以将这两者封装在独立的对象中,使它们各自独立地改变和复用。订阅发布模式所做的工作其实就是在解耦合。让耦合的双方都依赖于抽象,而不是依赖于具体,从而使得各自的变化都不会影响另一边的变化。
什么是“订阅-发布模式”?
订阅-发布模式:定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都可以得到通知。
了解过事件机制或者函数式编程的朋友,应该会体会到“订阅-发布模式”所带来的“时间解耦”和“空间解耦”的优点。借助函数式编程中闭包和回调的概念,可以很优雅地实现这种设计模式。
“订阅-发布模式” vs 观察者模式
订阅-发布模式和观察者模式概念相似,但在订阅-发布模式中,订阅者和发布者之间多了一层中间件:一个被抽象出来的信息调度中心。
但其实没有必要太深究 2 者区别,因为《Head First 设计模式》这本经典书都写了:发布+订阅=观察者模式。其核心思想是状态改变和发布通知。 在此基础上,根据语言特性,进行实现即可。
两者区别
- 在发布者/订阅者模式中,组件与观察者模式完全分离。在观察者模式中,主题和观察者松散耦合。
- 观察者模式主要是以同步方式实现的,即当发生某些事件时,主题调用其所有观察者的适当方法。发布服务器/订阅服务器模式主要以异步方式实现(使用消息队列)。
- 发布者/订阅者模式更像是一种跨应用程序模式。发布服务器和订阅服务器可以驻留在两个不同的应用程序中。它们中的每一个都通过消息代理或消息队列进行通信。
代码实现
JS 中一般用事件模型来代替传统的发布-订阅模式。任何一个对象的原型链被指向Event
的时候,这个对象便可以绑定自定义事件和对应的回调函数。
const Event = {
clientList: {},
// 绑定事件监听
listen(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
return true;
},
// 触发对应事件
trigger() {
const key = Array.prototype.shift.apply(arguments),
fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (let fn of fns) {
fn.apply(null, arguments);
}
return true;
},
// 移除相关事件
remove(key, fn) {
let fns = this.clientList[key];
// 如果之前没有绑定事件
// 或者没有指明要移除的事件
// 直接返回
if (!fns || !fn) {
return false;
}
// 反向遍历移除置指定事件函数
for (let l = fns.length - 1; l >= 0; l--) {
let _fn = fns[l];
if (_fn === fn) {
fns.splice(l, 1);
}
}
return true;
}
};
// 为对象动态安装 发布-订阅 功能
const installEvent = obj => {
for (let key in Event) {
obj[key] = Event[key];
}
};
let salesOffices = {};
installEvent(salesOffices);
// 绑定自定义事件和回调函数
salesOffices.listen(
"event01",
(fn1 = price => {
console.log("Price is", price, "at event01");
})
);
salesOffices.listen(
"event02",
(fn2 = price => {
console.log("Price is", price, "at event02");
})
);
salesOffices.trigger("event01", 1000);
salesOffices.trigger("event02", 2000);
salesOffices.remove("event01", fn1);
// 输出: false
// 说明删除成功
console.log(salesOffices.trigger("event01", 1000));