共计 7645 个字符,预计需要花费 20 分钟才能阅读完成。
学习了一段时间设计模式,当学到观察者模式和发布订阅模式的时候遇到了很大的问题,这两个模式有点类似,有点傻傻分不清楚,博客起因如此,开始对观察者和发布订阅开始了 Google
之旅。对整个学习过程做一个简单的记录。
观察者模式
当对象间存在一对多关系时,则使用观察者模式 (Observer Pattern
)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。在观察模式中共存在两个角色 观察者 (Observer)
与被观察者(Subject)
,然而观察者模式在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变自动通知它们。
其实观察者模式是一个或多个观察者对目标的状态感兴趣,它们通过将自己依附在目标对象之上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,就会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,它们可以简单的将自己从中分离。
在观察者模式中一共分为这么几个角色:
- Subject: 维护一系列观察者,方便添加或删除观察者
- Observer: 为那些在目标状态发生改变时需要获得通知的对象提供一个更新接口
- ConcreteSuject: 状态发生改变时,想 Observer 发送通知,存储 ConcreteObserver 的状态
- ConcreteObserver: 具体的观察者
举例
举一个生活中的例子,公司老板可以为下面的工作人员分配认为,如果老板作为被观察者而存在,那么下面所属的那些员工则就作为观察者而存在,为工作人员分配的任务来通知下面的工作人员应该去做哪些工作。
通过上面的例子可以对观察者模式有一个简单的认知,接下来结合下面的这张图来再次分析一下上面的例子。
如果 Subject = 老板
的话,那么Observer N = 工作人员
,如果细心观察的话会发现下图中莫名到的多了一个notify()
,那么上述例子中的工作就是notify()
。
图片源于网络, 侵权必删
既然各个关系已经屡清楚了,下面通过代码来实现一下上述的例子:
// 观察者队列
class ObserverList{constructor(){this.observerList = {};
}
Add(obj,type = "any"){if(!this.observerList[type]){this.observerList[type] = [];}
this.observerList[type].push(obj);
}
Count(type = "any"){return this.observerList[type].length;
}
Get(index,type = "any"){let len = this.observerList[type].length;
if(index > -1 && index < len){return this.observerList[type][index]
}
}
IndexOf(obj,startIndex,type = "any"){
let i = startIndex,
pointer = -1;
let len = this.observerList[type].length;
while(i < len){if(this.observerList[type][i] === obj){pointer = i;}
i++;
}
return pointer;
}
RemoveIndexAt(index,type = "any"){let len = this.observerList[type].length;
if(index === 0){this.observerList[type].shift();}
else if(index === len-1){this.observerList[type].pop();}
else{this.observerList[type].splice(index,1);
}
}
}
// 老板
class Boos {constructor(){this.observers = new ObserverList();
}
AddObserverList(observer,type){this.observers.Add(observer,type);
}
RemoveObserver(oberver,type){let i = this.observers.IndexOf(oberver,0,type);
(i != -1) && this.observers.RemoveIndexAt(i,type);
}
Notify(type){let oberverCont = this.observers.Count(type);
for(let i=0;i<oberverCont;i++){let emp = this.observers.Get(i,type);
emp && emp(type);
}
}
}
class Employees {constructor(name){this.name = name;}
getName(){return this.name;}
}
class Work {married(name){console.log(`${name}上班 `);
}
unemployment(name){console.log(`${name}出差 `);
}
writing(name){console.log(`${name}写作 `);
}
writeCode(name){console.log(`${name}打代码 `);
}
}
let MyBoos = new Boos();
let work = new Work();
let aaron = new Employees("Aaron");
let angie = new Employees("Angie");
let aaronName = aaron.getName();
let angieName = angie.getName();
MyBoos.AddObserverList(work.married,aaronName);
MyBoos.AddObserverList(work.writeCode,aaronName);
MyBoos.AddObserverList(work.writing,aaronName);
MyBoos.RemoveObserver(work.writing,aaronName);
MyBoos.Notify(aaronName);
MyBoos.AddObserverList(work.married,angieName);
MyBoos.AddObserverList(work.unemployment,angieName);
MyBoos.Notify(angieName);
// Aaron 上班
// Aaron 打代码
// Angie 上班
// Angie 出差
代码里面完全遵循了流程图,Boos
类作为被观察者而存在,Staff
作为观察者,通过 Work
两者做关联。
如果相信的阅读上述代码的话可以出,其实观察者的核心代码就是 peopleList
这个对象,这个对象里面存放了 N
多个 Array
数组,通过 run
方法触发观察者的 notify
队列。观察者模式主要解决的问题就是,一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。当我们在做程序设计的时候,当一个目标对象的状态发生改变,所有的观察者对象都将得到通知,进行广播通知的时候,就可以使用观察者模式啦。
优点
- 观察者和被观察者是抽象耦合的。
- 建立一套触发机制。
缺点
- 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
小结
对于观察者模式在被观察者中有一个用于存储观察者对象的 list
队列,通过统一的方法触发,目标和观察者是基类,目标提供维护观察者的一系列方法,观察者提供更新接口。具体观察者和具体目标继承各自的基类,然后具体观察者把自己注册到具体目标里,在具体目标发生变化时候,调度观察者的更新方法。
发布 / 订阅模式
在发布订阅模式上卡了很久,但是废了好长时间没有搞明白,也不知道自己的疑问在哪,于是就疯狂 Google
不断地翻阅找到自己的疑问,个人觉得如果想要搞明白发布订阅模式首先要搞明白谁是发布者,谁是订阅者。
发布订阅:在软件架构中,发布 - 订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。– 维基百科
看了半天没整明白(✿◡‿◡),惭愧 … 于是,学习的路途不能止步,继续 …
大概很多人都和我一样,觉得发布订阅模式里的 Publisher
,就是观察者模式里的Subject
,而Subscriber
,就是Observer
。Publisher
变化时,就主动去通知Subscriber
。其实并不是。在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。互不相识?那他们之间如何交流?
答案是,通过第三者,也就是在消息队列里面,我们常说的经纪人Broker
。
发布者只需告诉 Broker
,我要发的消息,topic
是AAA
, 订阅者只需告诉 Broker
,我要订阅topic
是AAA
的消息, 于是,当 Broker
收到发布者发过来消息,并且 topic
是AAA
时,就会把消息推送给订阅了 topic
是AAA
的订阅者。当然也有可能是订阅者自己过来拉取,看具体实现。
也就是说,发布订阅模式里,发布者和订阅者,不是松耦合,而是完全解耦的。
图片源于网络, 侵权必删
通过上面的描述终于有了一些眉目,再举一个生活中的例子,就拿微信公众号来说,每次微信公众号推送消息并不是一下子推送给微信的所有用户,而是选择性的推送给那些已经订阅了该公众号的人。
老规矩吧,用代码实现一下:
class Utils {constructor(){this.observerList = {};
}
Add(obj,type = "any"){if(!this.observerList[type]){this.observerList[type] = [];}
this.observerList[type].push(obj);
}
Count(type = "any"){return this.observerList[type].length;
}
Get(index,type = "any"){let len = this.observerList[type].length;
if(index > -1 && index < len){return this.observerList[type][index]
}
}
IndexOf(obj,startIndex,type = "any"){
let i = startIndex,
pointer = -1;
let len = this.observerList[type].length;
while(i < len){if(this.observerList[type][i] === obj){pointer = i;}
i++;
}
return pointer;
}
}
// 订阅者
class Subscribe extends Utils {};
// 发布者
class Publish extends Utils {};
// 中转站
class Broker {constructor(){this.publish = new Publish();
this.subscribe = new Subscribe();}
// 订阅
Subscribe(fn,key){this.subscribe.Add(fn,key);
}
// 发布
Release(fn,key){this.publish.Add(fn,key);
}
Run(key = "any"){
let publishList = this.publish.observerList;
let subscribeList = this.subscribe.observerList;
if(!publishList[key] || !subscribeList[key]) throw "No subscribers or published messages";
let pub = publishList[key];
let sub = subscribeList[key];
let arr = [...pub,...sub];
while(arr.length){let item = arr.shift();
item(key);
}
}
}
class Employees {constructor(name){this.name = name;}
getName(){let {name} = this;
return name;
}
receivedMessage(key,name){console.log(`${name}收到了 ${key}发来的消息 `);
}
}
class Public {constructor(name){this.name = name;}
getName(){let {name} = this;
return name;
}
sendMessage(key){console.log(`${key}发送了一条消息 `);
}
}
let broker = new Broker();
let SundayPublic = new Public("Sunday");
let MayPublic = new Public("May");
let Angie = new Employees("Angie");
let Aaron = new Employees("Aaron");
broker.Subscribe(() => {Angie.receivedMessage(SundayPublic.getName(),Angie.getName());
},SundayPublic.getName());
broker.Subscribe(() => {Angie.receivedMessage(SundayPublic.getName(),Aaron.getName());
},SundayPublic.getName());
broker.Subscribe(() => {Aaron.receivedMessage(MayPublic.getName(),Aaron.getName());
},MayPublic.getName());
broker.Release(MayPublic.sendMessage,MayPublic.getName());
broker.Release(SundayPublic.sendMessage,SundayPublic.getName());
broker.Run(SundayPublic.getName());
broker.Run(MayPublic.getName());
// Sunday 发送了一条消息
// Angie 收到了 Sunday 发来的消息
// Aaron 收到了 Sunday 发来的消息
// May 发送了一条消息
// Aaron 收到了 May 发来的消息
通过上面的输出结果可以得出,只要订阅过该公众号的用户,只要公众号发送一条消息,所有订阅过该条消息的用户都是可以收到这条消息。虽然代码有点多,但是确确实实能够体现发布订阅模式的魅力,很不错。
优点
- 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
- 发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变;同理卖家(发布者)它只需要将鞋子来货的这件事告诉订阅者(买家),他不管买家到底买还是不买,还是买其他卖家的。只要鞋子到货了就通知订阅者即可。
缺点
- 创建订阅者需要消耗一定的时间和内存。
- 虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护。
小结
发布订阅模式可以降低发布者与订阅者之间的耦合程度,两者之间从来不关系你是谁,你要作什么?订阅者只需要跟随发布者,若发布者发生变化就会通知订阅者应该也做出相对于的变化。发布者与订阅者之间不存在直接通信,他们所有的一切事情都是通过中介者相互通信,它过滤所有传入的消息并相应地分发它们。发布订阅模式可用于在不同系统组件之间传递消息的模式,而这些组件不知道关于彼此身份的任何信息。
观察者模式与发布订阅的区别
- 在
Observer
模式中,Observers
知道Subject
,同时Subject
还保留了Observers
的记录。然而,在发布者 / 订阅者中,发布者和订阅者不需要彼此了解。他们只是在消息队列或代理的帮助下进行通信。 - 在
Publisher / Subscriber
模式中,组件是松散耦合的,而不是Observer
模式。 - 观察者模式主要以同步方式实现,即当某些事件发生时,
Subject
调用其所有观察者的适当方法。发布者 / 订阅者在大多情况下是异步方式(使用消息队列)。 - 观察者模式需要在单个应用程序地址空间中实现。另一方面,发布者 / 订阅者模式更像是跨应用程序模式。
图片源于网络, 侵权必删
如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的。如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。发布订阅模式相比观察者模式多了个事件通道,订阅者和发布者不是直接关联的。目标和观察者是直接联系在一起的。观察者把自身添加到了目标对象中,可见和发布订阅模式差别还是很大的。在这种模式下,目标更像一个发布者,他让添加进来的所有观察者都执行了传入的函数,而观察者就像一个订阅者。虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布 / 订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布 / 订阅模式则不会。
总结
虽然在学习这两种模式的时候有很多的坎坷,最终还是按照自己的理解写出来了两个案例。或许理解的有偏差,如果哪里有问题,希望大家在下面留言指正,我会尽快做出修复的。
如果你和我一样喜欢前端的话,可以加 Qq 群:135170291,期待大家的加入。