引言
发布订阅者模式是一种很重要的设计模式,在很多软件架构中都作为核心部件组织代码,通常用于:分布式系统、消息队列、事件处理系统等、GUI框架等场景下。在前端领域很多框架也都在实现发布订阅者模式的思想,比如:Redux、Vue、RxJS、EventEmiiter3 等。发布订阅者模式核心构件有三个部分组成、发布者、订阅者、事件和数据。接下来我们一起揭开这层面纱,彻底理解发布订阅者模式的思路。
小故事
话说最近 IKUN 很烦恼,太多小黑子需要发律师函。 小黑子每天都在群里唱跳Rap, 基尼太美~~ 。IKUN 思来想去提交了知识产权申请,以后只能 IKUN 授权的小黑子才能唱:‘基尼太美~’ 其他未授权的监控到违规使用,立即发送律师函。
由于家里厕纸不太够了,我们需要帮IKUN实现这个想法,尽早收到律师函补贴家用。
这个场景完美契合发布订阅者模式: 发布者消息变化发送消息,订阅者检测变化做出响应。
我们秉承面向接口编程,先抽象一下发布者和订阅者接口。
订阅者 Observer 需要定义一个接收数据的函数 revive,这里的数据包含三个部分,type、name、 msg 。
发布者 Publisher
- 定义维护观察者的列表 subscribers 【按照最小暴露原则,这个部分不应该暴露出来,但是为了大家理解清楚,这里我们选择暴露出来】、
- 进行注册和注销观察者的两个方法 subscribe/unsubscribe 、
- 最后定义一个广播消息的函数 publish 。
interface Observer {
revive(data: { type: string; name: string; msg: string }): void
}
interface Publisher {
subscribers: Observer[]
subscribe(op: Observer): void
unsubscribe(op: Observer): void
publish(data: any): void
}
我们先来实现一下订阅者,也就是我们的 IKUN
IKUN 为了避免误伤,建立了一个白名单 whiteList 名单里面的都是授权的人群,比如刘德华要是唱跳rap了也不能起诉,得给华哥打钱感谢宣传。定义了一个发送律师函的方法 postPaper。
class IKUN implements Observer {
// IKUN 授权的白名单
private whiteList: string[] = []
constructor() {
this.whiteList = ["LiuDeHua"]
}
revive(action: { type: string; name: string; msg: string }): void {
const { type, msg, name } = action
// 坤坤 主要针对的都是男性的小黑子,女生一般都 比较温柔就不恐吓了
if (!this.whiteList.includes(name) && type === "male") {
// 不在白名单的,发生侵权都给我发律师函
if (msg.includes("基尼太美")) this.postPaper({ name, msg })
}
}
private postPaper(op: { name: string; msg: string }) {
const { name, msg } = op
console.log(`${name}你好,我是IKUN, 请停止你的侵权行为:${msg}`)
}
}
该定义我们的发布者了,也就是在座的各位。这里面我们比较自觉,微信里面聊了啥都大方的通知外部的订阅者~ 现实生活中我们可没这么傻,但是有好心人帮我们做了这件事~ 我们身边有个间谍,你说的话,打的字,都会被收集起来然后分发给下游,下游会在你的广告上、购物APP上给你精准推荐最近你搜索或者说过的物品。这个不是偶然~~~
所以对于坤坤,我们聊了什么直接发给他,避免中间商赚差价。
class IKUNFence implements Publisher {
subscribers: Observer[]
private info: { type: string; name: string }
constructor(name: string, type: "male" | "female") {
this.subscribers = []
this.info = {
name,
type,
}
}
// 注册观察者
subscribe(op: Observer): void {
// throw new Error("Method not implemented.")
this.subscribers.push(op)
}
// 注销观察者
unsubscribe(op: Observer): void {
// throw new Error("Method not implemented.")
const i = this.subscribers.indexOf(op)
this.subscribers.splice(i, 1)
}
// 微信群聊天呢
weChatSendMessage() {
this.publish("练习两年半,基尼太美~ oh 轰")
}
// 说了什么都通知订阅者
publish(data: string): void {
// throw new Error("Method not implemented.")
this.subscribers.forEach((op) => {
// 将数据组织,提供给订阅者
op.revive({
...this.info,
msg: data,
})
})
}
}
初始化程序,tony 是个男的,还不出名要是说点什么直接打死,xiaomei 是个女生、还有刘德华。 大家都微信群接龙了,但是只有 tony 收到了律师函~
function main() {
const ikun = new IKUN()
const ikunFences: IKUNFence[] = []
const tony = new IKUNFence("tony", "male")
tony.subscribe(ikun)
const xiaomei = new IKUNFence("xiaomei", "female")
xiaomei.subscribe(ikun)
const liudehua = new IKUNFence("LiuDeHua", "male")
liudehua.subscribe(ikun)
// 微信接龙开始
tony.weChatSendMessage()
xiaomei.weChatSendMessage()
liudehua.weChatSendMessage()
}
main()
故事到这里就已经结束了,tony 每天在群里接龙,家里厕纸瞬间充盈了起来。至此一个简单版本的发布订阅者模式就已经实现了,接下来我们深入的分析理解一下发布订阅者模式。
发布订阅者模式结构
发布者负责产生事件、消息或数据,而订阅者则表达对这些事件、消息或数据的兴趣,以便在发布者产生内容时被通知或接收到相应的数据。这种方式可以让不同的组件之间相互独立,而不需要显式地引用对方。
发布订阅者模式中的两个核心便是发布者、订阅者。要求发布者:需要定义维护订阅者的注册和注销函数,以及广播消息的能力。
订阅者需要接受数据消息的能力,针对每组 发布-订阅者 都需要约定好数据的格式。
发布订阅者模式的优点
- 松散耦合。发布者和订阅者之间的耦合关系由抽象层定义清楚:体现在两个部分,一个是发布者维护的都是订阅者抽象层次的修饰关系、另一个就是数据通信之间的依赖耦合(这个是必须一致的,不然就是风马牛)。对象之间的调用关系以通信的方式来进行,避免了对象之间的引用耦合关系。
- 建立一套触发机制,支持多对多通信。发布者可以通知多个订阅者,一个订阅者也可以监听多个发布者。只要这之间的协议是通用的,也就能实现多对多的通信。
- 扩展性。水平扩展发布者和订阅者,都是增量的扩展,对于已经存在的发布者和订阅者没有任何改造成本。
- 事件驱动。发布订阅者模式,由发布者发起事件,由对事件相关的订阅者来自行消费事件。这个在辅助异步通信架构、同步架构来解决不同的场景。异步通信主要是防止线程挂住,阻碍其他同步任务的执行。发布订阅者模式辅助多线程,我们可以实现分布式的并发计算。
发布订阅者模式的缺点
发布订阅者模式的缺点是可以被避免的,存在两个问题:
- 可能会形成多层级的触发链。发布者-> 订阅者->触发新的订阅者 -> 发布者a -> .... -> 订阅者z 。 牵一发而动全身,在代码设计的时候我们需要从架构上避免这种链式传播的形成,将传播链长度限制在2以内比较合适。
- 同步阻塞,一个发布者对应多个观察者时。前边的观察者执行卡壳了,后边的观察者都得等待。解决这个问题只能引入异步(异步则需要注意时序),或者强行中断函数执行(需要借助 generator 来封装一个执行器,执行器按照时间分片进行调度比如每片 10ms 超时了,直接停掉)。
最佳实践
在讨论最佳实践之前我们先看看发布订阅者模式的使用场景,特定的技术都是在解决特定的问题,只有在特定的问题范围下才能发挥出它本来的能力。
发布订阅者模式适用于任何需要实现组件之间松散耦合、异步通信和事件驱动的情况。实现组件之间的解耦、松散耦合、异步通信等情况下。主要集中在这样一些场景:
- 事件处理: 发布订阅者模式可以用于处理事件驱动的情况,例如用户交互、UI更新、按键响应等。各个组件可以订阅相关事件,以便在事件发生时做出相应的响应。
- 消息队列: 当需要在不同部分之间传递消息或任务时,发布订阅者模式可以用于构建消息队列系统,以实现松散耦合的异步通信。
- 状态管理: 在需要管理应用程序的状态、数据共享和变更通知的情况下,发布订阅者模式可以作为状态管理的一种实现方式,例如在 React 应用中使用 Redux。
- 插件架构: 发布订阅者模式可以用于实现插件架构,允许动态地添加、移除和交互不同的插件模块。
- 异步操作: 当需要执行异步操作(如网络请求、文件读写)并在操作完成后通知订阅者时,发布订阅者模式可以用来实现异步通信。
- 观察者模式: 观察者模式是发布订阅者模式的一个特例,适用于对象之间的依赖关系,当一个对象状态改变时,依赖于它的对象会收到通知。
- 多人协作: 在多人协作系统中,发布订阅者模式可以用于实现实时通信和共享数据,例如在线编辑工具、聊天应用等。
- 事件日志和审计: 发布订阅者模式可以用于记录事件日志和审计信息,以便在需要时对系统的活动进行跟踪和分析。
最佳实践是总结一些开源库中的实现方式以及社区里面大家形成的共识,目的是从扩展性和代码可读性上考虑,提升代码质量。明确如何实现好高质量的发布订阅者模式。
- 明确定义事件和命名: 定义清晰的事件名称和命名约定,以便开发人员可以轻松理解和使用。使用有意义的名词和动词来描述事件,以确保代码的可读性。比如 Redux 中 type 一般命名为 'pathName/methodName' 这样能够明确对 Reducer 的调用路径。
- 良好的文档和注释: 为发布者、订阅者和事件提供详细的文档和注释,解释其用途、参数和返回值。这可以帮助其他开发人员更好地理解和使用模式。借助 TypeScript 进行类型限制,确保协议上是一致的,复杂系统中文档必不可少。
- 避免过度使用: 尽量避免在不必要的地方使用发布订阅者模式,以免引入不必要的复杂性。仔细考虑是否需要松散耦合、异步通信和事件驱动才决定是否使用该模式。发布订阅者不是银弹,一定是符合使用场景,才能发挥它的威力,不然徒增代码量。
- 适当的取消订阅: 在订阅者不再需要订阅时,确保及时取消订阅,以避免内存泄漏。在组件销毁或不再需要事件通知时,执行取消订阅操作。
- 错误处理: 考虑如何处理订阅者可能出现的错误。在订阅者中适当地进行错误处理和异常处理,以保证应用程序的稳定性。
- 性能考虑: 虽然发布订阅者模式在许多情况下很有用,但在性能敏感的场景中,需要考虑事件通知的频率和开销。避免频繁触发大量事件,以及过多的事件监听器。
- 测试覆盖: 在实现发布订阅者模式时,编写适当的单元测试和集成测试,确保发布者和订阅者的行为符合预期。这个单元测试很重要,以前我从来不写单元测试,基本都是写完模块集成的时候再去做集成测试。局部的模块单元测试很有必要,能避免在集成的时候排查链路变长(出问题后,每个模块可能出现问题,单元测试后这样的问题,尽早的暴露出来了)。
- 中间件和增强: 如果需要在事件传递过程中执行一些附加操作,可以使用中间件或增强机制来扩展发布订阅者模式的功能,例如日志记录、性能监控等。
- 保持一致性: 在整个应用程序中保持一致的事件命名和模式使用,这可以减少混乱和困惑,使代码更加统一。
- 考虑并发: 如果你的应用程序需要处理并发事件,确保适当地处理同步和异步操作,以避免竞态条件和不一致性。
总结
本文介绍了发布订阅者模式的相关细节,参考IKUN发律师函的故事。以及发布订阅者的使用场景和最佳实践中需要注意的点。各位看官,咱们下期再见~ 接下来我们剖析一下 Redux 源码上的实现思路,并以面向对象的方式来实现一个基础版本的 Redux.