Loading...
墨滴

Covh

2021/08/11  阅读:30  主题:默认主题

设计模式-发布订阅

最近从上一家公司离职,经历了各种公司面试题的折磨,本菜鸡决定痛定思痛,熟悉下自己最不愿意接触的领域,设计模式,先从面试经常问到的EventBus开始学习😭😭

情景问题

  1. iphone专卖店里, macbook pro 售完了,由于我很喜欢,所以我每天都要去iphone专卖店看看,如果没有到货,那么我每次回来都会空手而归
  2. 终于我买到了Macbook Pro了😄。终于和大家都一样拥有了自己的开发本😊,然而Macbook Pro内置的邮件总会给我推送其官网的广告,有需要的还好,如果是我不需要的,那么每次收到这个推送就很烦了😡

解决办法

  1. 问题1中我和专卖店是两个对象,我是订阅者(subscriber),专卖店是发布者(publisher),有没有mac是一个状态(state)。
  2. 问题2中所有拥有mac本的开发人员是订阅者,而苹果官网是发布者

我们可以让发布者添加订阅机制,让每个对象(发布者)都拥有订阅或取消订阅发布者事件,还包含一个通知订阅者的方法,这样的话:

  • 购买mac的时候不需要频发去专卖店了,只需要等待专卖店通知我有货了,我再去取货。
  • 不需要苹果官方的广告通知,那么取消订阅,这样的话只会给订阅的开发人员推送信息

发布订阅模式简介

发布/订阅模式(Publish Subscribe Pattern)属于设计模式中的行为(Behavioral Patterns),该模式需要你定义一种订阅机制,可以在对象(发布者publisher)当事件发生时去通知所有观察(订阅)该对象的其他对象(订阅者Subscriber)。

简单来说,一个发布者拥有以下属性

  1. 一个用于存储订阅者对象列表成员变量(subscribers);
  2. 用于控制状态的一个字段(state)
  3. 添加订阅者的方法(subscribe)
  4. 删除订阅者的方法(unsubscribe)
  5. 通知订阅者的方法 (notify)

用白话文讲解这里的属性,简单的把发布者理解为专卖店,则

  1. subscribers是存储订阅者事件的数组可以理解为专卖店的通讯录,这个通讯录记录了所有需要购买商品的姓名电话和购买的商品名,这里的商品名可以理解为事件的名字
  2. state可以理解为专卖店 有没有某款产品的库存,有还是没有,有几个,等状态信息
  3. subscribe可以理解为我要预购你们家的某个产品,你专卖店把我的信息记录一下,就是专卖店记录的一个过程
  4. unsubscribe,可以理解为我已经让专卖店做了记录了,但是过了段时间,因为某些原因,我不想购买这款产品了,需要你们把我的信息从上次的采购列表里面删掉,后面有货了就不需要给我法通知了
  5. notify可以理解为专卖店有货了,需要通知客户的一个行为(action),当然,怎么通知客户呢,需要客户给一个联系方式,这个联系方式就是Subscriber需要定义的update方法,只有给到了联系方式,专卖店有货或者没有货的时候才能通知到你,因为订阅的人太多了,专卖店不可能记录各种联系方式,所以专卖店要求必须以某种联系方式作为联系,这里映射到代码里就是要求订阅者必须实现某个方法提供给发布者来调用

那么订阅者就需要一个方法去接收发布者传回来的回调方法了,并且可以接收发布者传给订阅者的参数

伪代码

发布者

class Publisher {
  construct(){
    this.state = null;
    this.subscribers = [];
  }
  /*添加订阅 */
  subscribe(event,listener){}
  /* 取消订阅 */
  unsubscribe(event,listener){}
  /* 通知订阅者 */
  notify(){}
}

订阅者必须实现一个更新方法(update),用来接受发布者的通知(状态更新)

订阅者

class Subscriber {
  //用来接受发布者状态更新后的返回给订阅者的上下文
  update(ctx){}
}

typescript实现代码

type SubscribeList = Subscriber[]
type NothingType = Error | void
/* 
 * 订阅者接口定义
 */

interface Subscriber {
    /*
     * 更新方法,可以拿到发布者的实例,也叫发布者的上下文,可以拿到发布者对象上的所有public的字段
     */

    update(v: Publisher): NothingType;
}
/* 
 * 发布者接口定义
 */

interface Publisher {
    // 状态
    state: number;
    // 订阅者事件集合
    subscribers: SubscribeList;
    // 订阅方法
    subscribe(sub: Subscriber): NothingType;
    //取消订阅
    unsubscribe(sub: Subscriber): NothingType;
    // 通知订阅者
    notify(): NothingType;
}
class PublisherImp implements Publisher {
    state: number;
    subscribers: SubscribeList;
    constructor() {
        this.state = 0;
        this.subscribers = [];
    }
    subscribe(sub: Subscriber): NothingType {
        const isExist = this.subscribers.indexOf(sub) === -1;
        if (isExist) {
            this.subscribers.push(sub);
        }
    }
    unsubscribe(sub: Subscriber): NothingType {
        const subIndex = this.subscribers.indexOf(sub);
        const isExist = subIndex === -1;
        if (!isExist) {
            this.subscribers.splice(subIndex, 1);
        }
    }
    notify(): NothingType {
        for (const subscribe of this.subscribers) {
            subscribe.update(this);
        }
    }
    // 模仿业务逻辑,修改状态,触发通知
    trigger() {
        this.state += 1
        this.notify();
        console.log("事件广播完毕----------------")
    }

}

class SubscriberImp1 implements Subscriber {
    update(v: Publisher): NothingType {
        // do something 
        console.log('sub1 get update info:' + v.state)
    }
}


class SubscriberImp2 implements Subscriber {
    update(v: Publisher): NothingType {
        // do something 
        console.log('sub2 get update info:' + v.state)
    }
}
// 创建一个发布者
const publisher = new PublisherImp();
// 第一个订阅者
const subscribe1 = new SubscriberImp1();
// 添加订阅到发布者,事件名称为 on
publisher.subscribe(subscribe1);
// 第二个订阅者
const subscribe2 = new SubscriberImp2();
publisher.subscribe(subscribe2);

setTimeout(() => {
    // 两秒后发布者广播on事件,此时订阅者1和2都会收到广播事件
    publisher.trigger();
}, 2000);

setTimeout(() => {
    // 5秒后移除订阅者2
    publisher.unsubscribe(subscribe2);
    // 重新广播,此时只有订阅者1会收到消息
    publisher.trigger();
}, 5000);

setTimeout(() => {
    // 再次触发,也只有订阅者1收到消息
    publisher.trigger();
}, 6000);

执行结果如下

sub1 get update info:1
sub2 get update info:1
事件广播完毕----------------
sub1 get update info:2
事件广播完毕----------------
sub1 get update info:3
事件广播完毕----------------

当然实际业务场景不是单个事件,是需要支持多种事件的订阅的,这里需要调整下代码,如下

- type SubscribeList = Subscriber[]
+ type SubscribeList = {
+    [p: string]: Subscriber[]
+ }
type NothingType = Error | void
/* 
 * 订阅者接口定义
 */
interface Subscriber {
    /*
     * 更新方法,可以拿到发布者的实例,也叫发布者的上下文,可以拿到发布者对象上的所有public的字段
     */
    update(v: Publisher): NothingType;
}
/* 
 * 发布者接口定义
 */
interface Publisher {
    // 状态
    state: number;
    // 订阅者事件集合
    subscribers: SubscribeList;
    // 订阅方法
-   subscribe(sub: Subscriber): NothingType;    
+   subscribe(event: string, sub: Subscriber): NothingType;
    //取消订阅
-   unsubscribe(sub: Subscriber): NothingType;
+   unsubscribe(event: string, sub: Subscriber): NothingType;
    // 通知订阅者
-   notify(): NothingType;
+   notify(event: string): NothingType;
}
class PublisherImp implements Publisher {
    state: number;
    subscribers: SubscribeList;
    constructor() {
        this.state = 0;
-       this.subscribers = [];
+       this.subscribers = {};
    }
-    subscribe(sub: Subscriber): NothingType {
-    const isExist = this.subscribers.indexOf(sub) === -1;
-       if (isExist) {
-           this.subscribers.push(sub);
-       }
-   }
-   unsubscribe(sub: Subscriber): NothingType {
-       const subIndex = this.subscribers.indexOf(sub);
-       const isExist = subIndex === -1;
-       if (!isExist) {
-           this.subscribers.splice(subIndex, 1);
-       }
-   }
-   notify(): NothingType {
-       for (const subscribe of this.subscribers) {
-           subscribe.update(this);
-       }
-   }
+ subscribe(event: string, sub: Subscriber): NothingType + {
+      // 事件不再订阅列表内,则添加一个空事件队列
+       if (!(event in this.subscribers)) {
+           this.subscribers[event] = []
+       }
+       this.subscribers[event].push(sub)
+   }
+   unsubscribe(event: string, sub: Subscriber): NothingType {
+       // 若没有注册该事件则抛出错误
+       if (!(event in this.subscribers)) {
+           return new Error("事件无效")
+       }
+       const subIndex = this.subscribers[event].indexOf(sub)
+       if (subIndex !== -1) {
+           this.subscribers[event].splice(subIndex, 1);

+           // 当前事件队列为空,则移除事件队列
+           if (this.subscribers[event].length === 0) {
+               delete this.subscribers[event]
+           }
+       } else {
+           return new Error("事件已移除")
+       }
+   }
+   notify(event: string): NothingType {
+       // 若没有注册该事件则抛出错误
+       if (!(event in this.subscribers)) {
+           return new Error('未注册该事件')
+       }
+       // 便利触发
+       this.subscribers[event].forEach(sub => {
+           sub.update(this);
+       })
+   }
+   // 模仿业务逻辑,修改状态,触发通知
-  trigger() {
+  trigger(event: string,) {
        this.state += 1
-       this.notify();
+       this.notify(event);
        console.log("事件广播完毕----------------")
    }

}

class SubscriberImp1 implements Subscriber {
    update(v: Publisher): NothingType {
        // do something 
        console.log('sub1 get update info:' + v.state)
    }
}


class SubscriberImp2 implements Subscriber {
    update(v: Publisher): NothingType {
        // do something 
        console.log('sub2 get update info:' + v.state)
    }
}
// 创建一个发布者
const publisher = new PublisherImp();
// 第一个订阅者
const subscribe1 = new SubscriberImp1();
// 添加订阅到发布者,事件名称为 on
- publisher.subscribe(subscribe1);
+ publisher.subscribe("on", subscribe1);
// 第二个订阅者
const subscribe2 = new SubscriberImp2();
- publisher.subscribe(subscribe2);
+ publisher.subscribe("on", subscribe2);

setTimeout(() => {
    // 两秒后发布者广播on事件,此时订阅者1和2都会收到广播事件
-   publisher.trigger();
+   publisher.trigger("on");
}, 2000);

setTimeout(() => {
    // 5秒后移除订阅者2
-   publisher.unsubscribe(subscribe2);
+   publisher.unsubscribe('on', subscribe2);
    
    // 重新广播,此时只有订阅者1会收到消息
-   publisher.trigger();
+   publisher.trigger("on");
}, 5000);

setTimeout(() => {
    // 再次触发,也只有订阅者1收到消息
-   publisher.trigger();
+   publisher.trigger("on");
}, 6000);

执行结果

sub1 get update info:1
sub2 get update info:1
事件广播完毕----------------
sub1 get update info:2
事件广播完毕----------------
sub1 get update info:3
事件广播完毕----------------

多种事件的时候,需要维护一个事件名和事件队列,放到对象中存储,注册事件或取消事件的时候,根据事件名称去查找对应的订阅者,之后对订阅者进行操作

更多文档

更多文档请参考 免费学习设计模式[1]

参考资料

[1]

设计模式: https://refactoringguru.cn/

Covh

2021/08/11  阅读:30  主题:默认主题

作者介绍

Covh