设计模式之-发布订阅者模式

发布订阅模式可以看作是观察者模式的一种升级版本,和观察者模式在结构上比较相似,但是它们在进行消息传递的时候,传递方式会有所不同。

  1. 观察者模式

直接通信:在观察者模式中,观察者(Observer)直接订阅主体(Subject)。当主体发生改变时,它直接通知这些观察者。

双向依赖:观察者需要知道主体,主体需要维护一个观察者列表

主体状态:观察者通常对主体的状态变化感兴趣,它们可能会从主体中检索状态。

实例:很多GUI工具使用观察者模式,例如,一个按钮(主体)可以有多个事件监听器(观察者)

  1. 发布订阅模式

间接通信:发布-订阅模式使用一个称作代理(Broker)或消息中介(Message Queue)的第三方组件,发布者(Publisher)不直接发送消息给订阅者(Subscriber),而是通过消息中介

解耦:发布者和订阅者不需要知道对方的存在。它们只需要知道相关的消息或事件类型。

异步通信:通常发布-订阅模式支持异步处理,订阅者可以在未来的任何时间点处理收到的消息

动态关系:订阅者可以根据需要随时订阅或取消订阅消息。

实例:消息队列(如 RabbitMQ)、流处理(如 Kafka)和许多现代应用框架使用发布-订阅模式

1.先来看一个简单的例子

javascript 复制代码
class EventBus{
    constructor(){
        this.events = {};
    }
    on(event,callback){
        this.events[event] = this.events[event] || [];
        this.events[event].push(callback);
    }
    emit(event,data){
        (this.events[event] || []).forEach(cb=>cb(data));
    }
}
// 使用
const bus = new EventBus();
bus.on('message',data=>{
    console.log('receive---',data)
})
bus.emit('message','hello,syt');

2.写一个ts版本的例子

下面这是一段伪代码,

typescript 复制代码
// request.ts文件
import axios,{ AxiosResponse } from 'axios';
import router from './router';
import { message } from 'ant-design-vue';

const ins = axios.create({
    baseURL:'http://localhost:3000',
});
const successHandler = (res:AxiosResponse):any=>{
    //略
}
const errorHandler = (error:any):any =>{
    if(error.response.status===401){
        //在失败的拦截器里面如果是401的,登陆过期,
        // 有一个提示弹窗
        message.error('登录失败,请重新登录');
        // 有一个路由跳转,逻辑是通的,但是这种写法不好,
        // 因为这个模块时在做网络请求的,网络的模块里面为啥会有组件库的依赖呢,网络里面为啥
        // 会对路由有依赖呢,这就很奇怪,这种一来就会造成耦合,就是网络跟我们的组件,界面耦合了,以及路由耦合了
        // 那么耦合会带来什么问题呢,耦合带来的问题一定是,你耦合了什么东西,那么你耦合的东西一变,你这里很有可能会跟着变化,
        // 这就会导致一个诡异的现象,将来界面需求变化了,你要去动网络
        // 比如界面的逻辑不仅仅要处理401还要处理400的逻辑,
        // 也有可能将来有一天登录失败不跳转到登录页面,而是弹出一个弹出层,在弹出层上登录,那这个问题就更加麻烦了
        // 那么这个耦合可能以后会越来越高,直到最后你会发现这个模块10%是在处理网络,90%是在处理路由和界面,那就完全没法看
        // 也就意味着你的工程将来很难维护
        router.push('/login');
    }
}

ins.interceptors.response.use(successHandler,errorHandler);

那么我们需要解藕,那么怎么做呢,那就是加中间层,我们先把他叫事件中心,一个模块发生了一件事,我不知道干嘛,比如发生了401了,你不知道干嘛也不要自作主张,这个事发生了,就抛出事件,让其别的模块监听事件,别的模块监听到事件就该干嘛干嘛,该是路由做的事情,就由路由去做,该是界面做的事就由界面去做,这样就解藕了,然后我们定义一个公共模块,即事件中心

typescript 复制代码
// eventEmitter.ts文件

const eventNames=['API:UN_AUTH','API:INVALID'];
type EventNames = (typeof eventNames)[number];


class EventEmitter {
    private listeners:Record<string,Set<Function>> = {
        'API:UN_AUTH':new Set(),
        'API:INVALID':new Set(),
    };
    on(eventName:EventNames,listener:Function){
        this.listeners[eventName].add(listener);
    }
    emit(eventName:EventNames,...args:any[]){
        this.listeners[eventName].forEach(listener=>listener(...args));
    }
}

export default new EventEmitter();

然后这个网络模块里面就不要咬和界面以及路由耦合到一起了,直接引入事件中心,通过它来发送消息

typescript 复制代码
// request.ts文件
import axios,{ AxiosResponse } from 'axios';
import emitter from './eventEmitter';
const ins = axios.create({
    baseURL:'http://localhost:3000',
});
const successHandler = (res:AxiosResponse):any=>{
    //略
}
const errorHandler = (error:any):any =>{
    if(error.response.status===401){
        emitter.emit('API:UN_AUTH')
    }else if(error.response.status===400){
        emitter.emit('API:INVALID')
    }
}

ins.interceptors.response.use(successHandler,errorHandler);

将来有一天路由要处理的话,那就导入事件中心,自己去处理自己该干的事,比如跳转路由,界面也是一样的,各自模块各自处理自己的事情

typescript 复制代码
//router.ts
import {createRouter,createWebHashHistory} from 'vue-router';
import emitter from './eventEmitter';

const router = createRouter({
    history:createWebHashHistory(),
    routes:[],
});
// 注册事件
emitter.on('API:UN_AUTH',()=>{
    router.push('/login')
});
export default router;

3.demo3

typescript 复制代码
// 发布者接口
interface IPublisher {
    // 发布者只管发布消息
    publish:(topic:string,message:string)=>void;
}
// 订阅者接口
// 现在是由订阅者自己来决定订阅哪个主题
interface ISubscriber {
    subscribe:(topic:string) => void;
    unsubscribe:(topic:string) => void;
    receive:(message:string) => void;
}
// 中间人接口
interface IBroker {
    // 订阅主题
    subscribe:(topic:string,subscriber:Subscriber)=>void;
    // 取消订阅
    unsubscribe:(topic:string,subscriber:Subscriber)=>void;
    // 发布消息
    publish:(topic:string,message:any)=>void;
}
// 中间人
// 回头发布者要发布消息,就会通知中间人,中间人再通知所有订阅者
// 并且订阅者的列表也是由中间人维护的

// 中间人要做的事情:1.帮发布者发布消息2.帮订阅者订阅主题消息
class Broker implements IBroker {
    // 内部维护一个主题和订阅者的列表
    private topics: Map<string, ISubscriber[]> = new Map();

    // 订阅方法
    subscribe(topic: string, subscriber: ISubscriber):void  {
        // 拿到对应的主题的观察者列表(订阅者列表)
        const subTopicSubscribers = this.topics.get(topic) || [];
        subTopicSubscribers.push(subscriber);
        this.topics.set(topic,subTopicSubscribers);

    };
    // 取消订阅方法
    unsubscribe(topic: string, subscriber: ISubscriber):void {
        const subTopicSubscribers = this.topics.get(topic)||[];
        const index = subTopicSubscribers.indexOf(subscriber);
        if(index!==-1){
            subTopicSubscribers.splice(index,1);
        }
    };
    // 发布消息方法
    publish (topic: string, message: any):void {
        // 获取到订阅了这个主题的订阅者列表
        const subTopicSubscribers = this.topics.get(topic) || [];
        // 遍历所有的订阅者,通知他们
        for(const subscriber of subTopicSubscribers){
            subscriber.receive(message);
        }
    };
    
}

//订阅者
class Subscriber implements ISubscriber {
    private id:number;
    private broker:IBroker;

    constructor(id:number,broker:IBroker){
        this.id = id;
        this.broker = broker;
    }
    subscribe(topic: string):void{
        // 订阅者要订阅哪一个主题,向中间人报名
        this.broker.subscribe(topic,this);
    };
    unsubscribe(topic: string):void {
        // 订阅者要取消订阅哪一个主题
        this.broker.unsubscribe(topic,this);

    };
    receive (message: string):void {
        console.log(`订阅者${this.id}接收到消息:${message}`);

    };
    
}
// 发布者
class Publisher implements IPublisher {
    private broker:IBroker;
    constructor(broker:IBroker){
        this.broker = broker;
    }
    // 发布消息的方法
    // 要发布具体的消息,我们只需要将消息交给中间人即可
    publish(topic:string,message:string):void {
        console.log(`发布者发了一个${topic}主题的消息:${message}`);
        // 通知中间人向指定主题发布消息
        this.broker.publish(topic,message);
    }
}

// 使用
// 创建一个中间人
const broker = new Broker();
// 创建一个发布者
const publisher = new Publisher(broker);

// 创建四个订阅者
const subscribe1 = new Subscriber(1,broker);
const subscribe2 = new Subscriber(2,broker);
const subscribe3 = new Subscriber(3,broker);
const subscribe4 = new Subscriber(4,broker);

// 四个订阅者订阅主题
subscribe1.subscribe('动作片');
subscribe2.subscribe('动作片');
subscribe3.subscribe('恐怖片');
subscribe4.subscribe('恐怖片');

publisher.publish("恐怖片","恐怖片上映了");

subscribe3.unsubscribe("恐怖片");

publisher.publish('恐怖片','咒怨终结版上映了')

非原创,来源渡一袁老师和谢杰老师,简单记录下吧

相关推荐
触底反弹5 小时前
🤯 面试被问 AI Workflow 和 Agent 有啥区别?3 张图 + 2 段代码讲清楚!
人工智能·设计模式·面试
杨充1 天前
10.可测试性实战设计
设计模式·开源·代码规范
杨充1 天前
9.重构十二式的实战
设计模式·开源·代码规范
杨充1 天前
6.设计原则的全景图
设计模式·开源·全栈
杨充1 天前
2.面向对象的特性
设计模式
杨充1 天前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充1 天前
8.反模式与坏味道
设计模式·开源·代码规范
杨充1 天前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法