用JS写一个发布订阅模式
目录
- 1 场景引入
- 2 代码优化
- 2.1 解决增加粉丝问题
- 2.2 解决添加作品问题
- 3 观察者模式
- 4 经纪人登场
- 5 发布订阅模式
- 6 观察者模式和发布订阅模式的对比
1 场景引入 我们先来看这么一个场景:
假设现在有一个社交平台,平台上有一个大V叫
Nami
Nami很牛,多才多艺,目前她有2个技能:会写歌、会拍视频
她会把这些作品发布到平台上。关注她的粉丝就会接收到这些内容
现在他已经有3个粉丝了,分别是:Luffy、Zoro、Sanji
每次只要Nami一发布作品,3个粉丝的账号上收到的消息就会更新
现在用代码来表示:
const luffy = {update: function (songs, videos) {console.log(songs, videos); },}; const zoro = {update: function (songs, videos) {console.log(songs, videos); },}; const sanji = {update: function (songs, videos) {console.log(songs, videos); },}; const nami = {// 只要Nami的作品一更新,这个方法就会被调用workUpdate: function () {// 获取作品const songs = this.getSongs(); const videos = this.getVideos(); // 账号更新luffy.update(songs, videos); zoro.update(songs, videos); sanji.update(songs, videos); },getSongs: function () {return "mp3"; },getVideos: function () {return "mp4"; },};
现在问题来了
- 如果
Nami
又收获了一个粉丝Robin
,我既要添加一个robin
对象,又要修改workUpdate
方法 - 如果
Nami
又有了一项新技能:写小说,我既要修改workUpdate
函数,又要修改每个粉丝对象中的update
方法,因为参数增加了一个
粉丝对象和大V对象之间的耦合度太高,导致两者很难各自扩展
2 代码优化
2.1 解决增加粉丝问题
先来解决上述第1个问题,使得增加粉丝的时候不用再修改
workUpdate
方法首先,我们将“大V”抽象成一个类Star,用数组
fans
来保存粉丝列表,并新增一个添加粉丝的方法addFans
class Star {constructor() {this.fans = []; }addFans(fan) {this.fans.push(fan)}workUpdate() {const songs = this.getSongs(); const videos = this.getVideos(); this.fans.forEach((item) => item.update(songs, videos)); }getSongs() {return "MP3"; }getVideos() {return "MP4"; }}
接着,将“粉丝”也抽象成一个类Fan,我们在创建粉丝对象的时候传入“大V”对象,调用该大V的
addFans
方法来添加到粉丝列表class Fan {constructor(name, star) {this.name = namethis.star = starthis.star.addFans(this)}update(songs, videos) {console.log(songs, videos); }}
现在我们添加粉丝就不必再更改代码了
const nami = new Star()const luffy = new Fan("luffy", nami); const zoro = new Fan("zoro", nami); const sanji = new Fan("sanji", nami); const robin = new Fan("robin", nami); nami.workUpdate()
2.2 解决添加作品问题
我们新增一个
works
数组来保存大V的作品,并且为其添加get
和set
方法class Star {constructor() {this.fans = []; this.works = []; }addFans(fan) {this.fans.push(fan); }setWorks(work) {this.works.push(work); // 添加作品后,调用更新方法this.workUpdate(); }getWorks() {return this.works; }workUpdate() {this.fans.forEach((item) => item.update()); }}
对类Fan进行相应修改:
class Fan {constructor(name, star) {this.name = namethis.star = starthis.star.addFans(this)}update() {console.log(`${this.name}:${this.star.getWorks()}`)}}
现在大V添加作品就不必再更改代码了:
const nami = new Star(); nami.setWorks('song')nami.setWorks('video')nami.setWorks('novel')const luffy = new Fan("luffy", nami); const zoro = new Fan("zoro", nami); const sanji = new Fan("sanji", nami); nami.workUpdate();
3 观察者模式 可以看到,在上述例子中,一个
nami
对象和多个粉丝对象之间存在着一种一对多的依赖关系,当nami
对象有作品更新的时候,所有关注她的粉丝对象都会收到通知。事实上,这就是观察者模式
观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新我们将2.2中的代码进行进一步的抽象:
将“粉丝”看作观察者(
Observer
),将“大V”看作被观察的对象,称为主题(Subject
)Subject
维护一个观察者列表observerList
(原fans数组)。当Subject
的状态发生变化(原作品更新)时,通过调用notify(原workUpdate
)方法通知所有观察者,执行它们的update
方法具体代码如下:
// 被观察者:主题class Subject {constructor() {this.observerList = []; // 代表主题状态this.state = 0; }addObserver(observer) {this.observerList.push(observer); }// 更改主题状态setState(state) {this.state = state; // 状态改变后,通知所有观察者this.notify(); }getState() {return this.state; }notify() {this.observerList.forEach((observer) => observer.update()); }}// 观察者class Observer {constructor(name, subject) {this.name = name; this.subject = subject; this.subject.addObserver(this); }update() {console.log(`${this.name}:${this.subject.state}`); }}
4 经纪人登场 由于大V业务繁忙,所以他们需要经纪人来维持艺人与粉丝的联系
经纪人的工作包括:
- 维护大V的粉丝,经纪人手中会有一个粉丝名单
- 大V的新作品会交给经纪人,经纪人则负责把新作品发送给粉丝名单中的粉丝
class Manager {constructor() {this.fans = []; this.works = []; }addFans(fan) {this.fans.push(fan); }setWorks(work) {this.works.push(work); // 添加作品后,调用更新方法this.workUpdate(); }getWorks() {return this.works; }workUpdate() {this.fans.forEach((item) => item.update()); }}
嗯?这段代码貌似在哪儿见过?
没错,和2.2的Star类一模一样,只不过把类名改了改。
那这么做有意义吗?
事实上,代码一模一样是因为在2.2的Star类中我们只写了有关发布(即发布作品)和订阅(即维护粉丝列表)的功能;而Star类本身可能不止这个工作,比如创作内容。
现在我们将
Star
类中的发布和订阅的工作抽离出来,交给Manager
全权负责。而Star
类只要在创作完成后把作品交给Manager
就可以了另一方面,粉丝
Fan
也不再直接和Star
发生交互了,Fan只关心能不能收到作品,所以Fan
直接和Manager
发生交互,Fan去订阅(这个行为相当于在Manager
维护的粉丝列表中添加粉丝)Manager
并从Manager
那儿获取想要的作品于是Star和Fan的代码如下:
class Star {constructor() {}// 创作create(manager) {// 将创作的new work交给经纪人manager.setWorks("new work"); }}class Fan {constructor(name, manager) {this.name = name; this.manager = manager; this.manager.addFans(this); }update() {console.log(`${this.name}:${this.manager.getWorks()}`); }}
【用JS写一个发布订阅模式】
5 发布订阅模式 前面我们用了经纪人来负责发布和订阅的工作,而不让
Star
和Fan
发生直接交互,达到了两者解耦的效果这就是发布订阅模式
我们将4中的Manager进行进一步的抽象:
将“粉丝”看作订阅者(
Subscriber
);将“大V”看作内容的发布者,在发布订阅模式中称为发布者(Publisher
);把“经纪人”看作发布订阅中心(或者说中间人Broker
)具体代码如下:
// 发布订阅调度中心class Broker {constructor() {this.subscribers = []; // 代表主题状态this.state = 0; }// 订阅subscribe(subscriber) {this.subscribers.push(subscriber); }// 更改主题状态setState(state) {this.state = state; // 状态改变后,发布this.publish(); }getState() {return this.state; }// 发布publish() {this.subscribers.forEach((subscriber) => subscriber.update()); }}// 发布者class Publisher {constructor() {}changeState(broker, state) {broker.setState(state); }}class Subscriber {constructor(name, broker) {this.name = name; this.broker = broker; this.broker.subscribe(this); }update() {console.log(`${this.name}:${this.broker.getState()}`); }}
来运行一下看看效果:
// 创建调度中心const broker = new Broker()// 创建发布者const publisher = new Publisher()// 创建订阅者const subscribe1 = new Subscriber('s1', broker)const subscribe2 = new Subscriber('s2', broker)const subscribe3 = new Subscriber('s3', broker)// 发布者改变状态并通知调度中心,调度中心就会通知各个订阅者publisher.changeState(broker, 1)
6 观察者模式和发布订阅模式的对比
从角色数量看
- 观察者模式只有两个角色:观察者和被观察者
- 发布订阅模式有三个角色:发布者、订阅者以及中间人(发布订阅中心)
- 观察者模式处于一种松耦合的状态,即两者依然有交互,但是又很容易各自扩展且不相互影响
- 发布订阅模式中的发布者和订阅者则完全不存在耦合,达到了对象之间解耦的效果
- 两者都:实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知并自动更新
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 一个人的旅行,三亚
- 一个小故事,我的思考。
- Docker应用:容器间通信与Mariadb数据库主从复制
- 一个人的碎碎念
- JS中的各种宽高度定义及其应用
- 七年之痒之后
- 我从来不做坏事
- 由浅入深理解AOP
- 异地恋中,逐渐适应一个人到底意味着什么()