天天看點

【設計模式】692- TypeScript 設計模式之釋出-訂閱模式

【設計模式】692- TypeScript 設計模式之釋出-訂閱模式

前言

在之前兩篇自測清單中,和大家分享了很多 JavaScript 基礎知識,大家可以一起再回顧下~

本文是我在我們團隊内部“「現代 JavaScript 突擊隊」”分享的一篇内容,第二期學習内容為“「設計模式」”系列,我會将我負責分享的知識整理成文章輸出,希望能夠和大家一起溫故知新!

一、模式介紹

1. 生活場景

最近剛畢業的學生 Leo 準備開始租房了,他來到房産中介,跟中介描述了自己的租房需求,開開心心回家了。第二天,中介的小哥哥小姐姐為 Leo 列出符他需求的房間,并打電話約他一起看房了,最後 Leo 選中一套滿意的房間,高高興興過去簽合同,準備開始新生活~

還有個大佬 Paul,準備将手中 10 套房出租出去,于是他來到房産中介,在中介那邊提供了自己要出租的房間資訊,溝通好手續費,開開心心回家了。第二天,Paul 接到中介的好消息,房子租出去了,于是他高高興興過去簽合同,開始收房租了~

【設計模式】692- TypeScript 設計模式之釋出-訂閱模式

釋出-訂閱模式

上面場景有個需要特别注意的地方:

  • 租戶在租房過程中,不知道房間具體房東是誰,到後面簽合同才知道;
  • 房東在出租過程中,不知道房間具體租戶是誰,到後面簽合同才知道;

這兩點其實就是後面要介紹的 「釋出-訂閱模式」 的一個核心特點。

2. 概念介紹

在軟體架構中,釋出-訂閱模式是一種消息範式,消息的發送者(稱為釋出者)「不會将消息直接發送給特定的接收者」(稱為訂閱者)。而是将釋出的消息分為不同的類别,無需了解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達對一個或多個類别的興趣,隻接收感興趣的消息,無需了解哪些釋出者(如果有的話)存在。

釋出-訂閱是消息隊列範式的兄弟,通常是更大的面向消息中間件系統的一部分。大多數消息系統在API中同時支援消息隊列模型和釋出/訂閱模型,例如Java消息服務(JMS)。

這種模式提供了更大的網絡可擴充性和更動态的網絡拓撲,同時也降低了對釋出者和釋出資料的結構修改的靈活性。

二、 觀察者模式 vs 釋出-訂閱模式

看完上面概念,有沒有覺得與觀察者模式很像?但其實兩者還是有差異的,接下來一起看看。

1. 概念對比

我們分别為通過兩種實際生活場景來介紹這兩種模式:

  • 「觀察者模式」:如微信中 「顧客-微商」 關系;
  • 「釋出-訂閱模式」:如淘寶購物中 「顧客-淘寶-商家」 關系。

這兩種場景的過程分别是這樣:

1.1 觀察者模式

【設計模式】692- TypeScript 設計模式之釋出-訂閱模式

「觀察者模式」中,消費顧客關注(如加微信好友)自己有興趣的微商,微商就會私聊發自己在賣的産品給消費顧客。這個過程中,消費顧客相當于觀察者(Observer),微商相當于觀察目标(Subject)。

1.2 釋出-訂閱模式

接下來看看 「釋出-訂閱模式」 :

【設計模式】692- TypeScript 設計模式之釋出-訂閱模式

在 「釋出-訂閱模式」 中,消費顧客通過淘寶搜尋自己關注的産品,商家通過淘寶釋出商品,當消費顧客在淘寶搜尋的産品,已經有商家釋出,則淘寶會将對應商品推薦給消費顧客。這個過程中,消費顧客相當于訂閱者,淘寶相當于事件總線,商家相當于釋出者。

2. 流程對比

【設計模式】692- TypeScript 設計模式之釋出-訂閱模式

觀察者模式和釋出-訂閱模式差別

3. 小結

是以可以看出,「觀察者模式」和「釋出-訂閱模式」差别在于「有沒有一個中央的事件總線」。如果有,我們就可以認為這是個「釋出-訂閱模式」。如果沒有,那麼就可以認為是「觀察者模式」。因為其實它們都實作了一個關鍵的功能:「釋出事件-訂閱事件并觸發事件」。

三、模式特點

對比完「觀察者模式」和「釋出-訂閱模式」後,我們大緻了解「釋出-訂閱模式」是什麼了。接着總結下該模式的特點:

1. 模式組成

在釋出-訂閱模式中,通常包含以下角色:

  • 「釋出者:Publisher」
  • 「事件總線:Event Channel」
  • 「訂閱者:Subscriber」

2. UML 類圖

【設計模式】692- TypeScript 設計模式之釋出-訂閱模式

釋出-訂閱模式(UML)

3. 優點

  1. 松耦合(Independence)

「釋出-訂閱模式」可以将衆多需要通信的子系統(Subsystem)解耦,每個子系統獨立管理。而且即使部分子系統取消訂閱,也不會影響「事件總線」的整體管理。「釋出-訂閱模式」中每個應用程式都可以專注于其核心功能,而「事件總線」負責将消息路由到每個「訂閱者」手裡。

  1. 高伸縮性(Scalability)

「釋出-訂閱模式」增加了系統的可伸縮性,提高了釋出者的響應能力。原因是「釋出者」(Publisher)可以快速地向輸入通道發送一條消息,然後傳回到其核心處理職責,而不必等待子系統處理完成。然後「事件總線」負責確定把消息傳遞到每個「訂閱者」(Subscriber)手裡。

  1. 高可靠性(Reliability)

「釋出-訂閱模式」提高了可靠性。異步的消息傳遞有助于應用程式在增加的負載下繼續平穩運作,并且可以更有效地處理間歇性故障。

  1. 靈活性(Flexibility)

你不需要關心不同的元件是如何組合在一起的,隻要他們共同遵守一份協定即可。「釋出-訂閱模式」允許延遲處理或者按計劃的處理。例如當系統負載大的時候,訂閱者可以等到非高峰時間才接收消息,或者根據特定的計劃處理消息。

4. 缺點**

  1. 在建立訂閱者本身會消耗記憶體,但當訂閱消息後,沒有進行釋出,而訂閱者會一直儲存在記憶體中,占用記憶體;
  2. 建立訂閱者需要消耗一定的時間和記憶體。如果過度使用的話,反而使代碼不好了解及代碼不好維護。

四、使用場景

如果我們項目中很少使用到訂閱者,或者與子系統實時互動較少,則不适合 「釋出-訂閱模式」 。在以下情況下可以考慮使用此模式:

  1. 應用程式需要「向大量消費者廣播資訊」。例如微信訂閱号就是一個消費者量龐大的廣播平台。
  2. 應用程式需要與一個或多個獨立開發的應用程式或服務「通信」,這些應用程式或服務可能使用不同的平台、程式設計語言和通信協定。
  3. 應用程式可以向消費者發送資訊,而不需要消費者的實時響應。

五、實戰示例

1. 簡單示例

  1. 定義「釋出者接口」(Publisher)、「事件總線接口」(EventChannel)和「訂閱者接口」(Subscriber):
interface Publisher<T> {
  subscriber: string;
  data: T;
}

interface EventChannel<T>  {
  on  : (subscriber: string, callback: () => void) => void;
  off : (subscriber: string, callback: () => void) => void;
  emit: (subscriber: string, data: T) => void;
}

interface Subscriber {
  subscriber: string;
  callback: () => void;
}

// 友善後面使用
interface PublishData {
  [key: string]: string;
}      
  1. 實作「具體釋出者類」(ConcretePublisher):
class ConcretePublisher<T> implements Publisher<T> {
  public subscriber: string = "";
  public data: T; 
  constructor(subscriber: string, data: T) {
    this.subscriber = subscriber;
    this.data = data;
  }
}      
  1. 實作「具體事件總線類」(ConcreteEventChannel):
class ConcreteEventChannel<T> implements EventChannel<T> {
  // 初始化訂閱者對象
  private subjects: { [key: string]: Function[] } = {};

  // 實作添加訂閱事件
  public on(subscriber: string, callback: () => void): void {
    console.log(`收到訂閱資訊,訂閱事件:${subscriber}`);
    if (!this.subjects[subscriber]) {
      this.subjects[subscriber] = [];
    }
    this.subjects[subscriber].push(callback);
  };

  // 實作取消訂閱事件
  public off(subscriber: string, callback: () => void): void {
    console.log(`收到取消訂閱請求,需要取消的訂閱事件:${subscriber}`);
    if (callback === null) {
      this.subjects[subscriber] = [];
    } else {
      const index: number = this.subjects[subscriber].indexOf(callback);
      ~index && this.subjects[subscriber].splice(index, 1);
    }
  };
  
  // 實作釋出訂閱事件
  public emit (subscriber: string, data: T): void {
    console.log(`收到釋出者資訊,執行訂閱事件:${subscriber}`);
    this.subjects[subscriber].forEach(item => item(data));
  };
}      
  1. 實作「具體訂閱者類」(ConcreteSubscriber):
class ConcreteSubscriber implements Subscriber {
  public subscriber: string = "";
  constructor(subscriber: string, callback: () => void) {
    this.subscriber = subscriber;
    this.callback = callback;
  }
  public callback(): void { };
}      
  1. 運作示例代碼:
interface Publisher<T> {
  subscriber: string;
  data: T;
}

interface EventChannel<T>  {
  on  : (subscriber: string, callback: () => void) => void;
  off : (subscriber: string, callback: () => void) => void;
  emit: (subscriber: string, data: T) => void;
}

interface Subscriber {
  subscriber: string;
  callback: () => void;
}

interface PublishData {
  [key: string]: string;
}

class ConcreteEventChannel<T> implements EventChannel<T> {
  // 初始化訂閱者對象
  private subjects: { [key: string]: Function[] } = {};

  // 實作添加訂閱事件
  public on(subscriber: string, callback: () => void): void {
    console.log(`收到訂閱資訊,訂閱事件:${subscriber}`);
    if (!this.subjects[subscriber]) {
      this.subjects[subscriber] = [];
    }
    this.subjects[subscriber].push(callback);
  };

  // 實作取消訂閱事件
  public off(subscriber: string, callback: () => void): void {
    console.log(`收到取消訂閱請求,需要取消的訂閱事件:${subscriber}`);
    if (callback === null) {
      this.subjects[subscriber] = [];
    } else {
      const index: number = this.subjects[subscriber].indexOf(callback);
      ~index && this.subjects[subscriber].splice(index, 1);
    }
  };
  
  // 實作釋出訂閱事件
  public emit (subscriber: string, data: T): void {
    console.log(`收到釋出者資訊,執行訂閱事件:${subscriber}`);
    this.subjects[subscriber].forEach(item => item(data));
  };
}

class ConcretePublisher<T> implements Publisher<T> {
  public subscriber: string = "";
  public data: T; 
  constructor(subscriber: string, data: T) {
    this.subscriber = subscriber;
    this.data = data;
  }
}

class ConcreteSubscriber implements Subscriber {
  public subscriber: string = "";
  constructor(subscriber: string, callback: () => void) {
    this.subscriber = subscriber;
    this.callback = callback;
  }
  public callback(): void { };
}


/* 運作示例 */
const pingan8787 = new ConcreteSubscriber(
  "running",
  () => { 
    console.log("訂閱者 pingan8787 訂閱事件成功!執行回調~");
  }
);

const leo = new ConcreteSubscriber(
  "swimming",
  () => { 
    console.log("訂閱者 leo 訂閱事件成功!執行回調~");
  }
);

const lisa = new ConcreteSubscriber(
  "swimming",
  () => { 
    console.log("訂閱者 lisa 訂閱事件成功!執行回調~");
  }
);

const pual = new ConcretePublisher<PublishData>(
  "swimming",
  {message: "pual 釋出消息~"}
);

const eventBus = new ConcreteEventChannel<PublishData>();
eventBus.on(pingan8787.subscriber, pingan8787.callback);
eventBus.on(leo.subscriber, leo.callback);
eventBus.on(lisa.subscriber, lisa.callback);

// 釋出者 pual 釋出 "swimming"相關的事件
eventBus.emit(pual.subscriber, pual.data); 
eventBus.off (lisa.subscriber, lisa.callback);
eventBus.emit(pual.subscriber, pual.data);

/*
輸出結果:
[LOG]: 收到訂閱資訊,訂閱事件:running
[LOG]: 收到訂閱資訊,訂閱事件:swimming
[LOG]: 收到訂閱資訊,訂閱事件:swimming
[LOG]: 收到釋出者資訊,執行訂閱事件:swimming
[LOG]: 訂閱者 leo 訂閱事件成功!執行回調~ 
[LOG]: 訂閱者 lisa 訂閱事件成功!執行回調~ 
[LOG]: 收到取消訂閱請求,需要取消的訂閱事件:swimming
[LOG]: 收到釋出者資訊,執行訂閱事件:swimming
[LOG]: 訂閱者 leo 訂閱事件成功!執行回調~ 
*/      

完整代碼如下:

interface Publisher {
  subscriber: string;
  data: any;
}

interface EventChannel {
  on  : (subscriber: string, callback: () => void) => void;
  off : (subscriber: string, callback: () => void) => void;
  emit: (subscriber: string, data: any) => void;
}

interface Subscriber {
  subscriber: string;
  callback: () => void;
}

class ConcreteEventChannel implements EventChannel {
  // 初始化訂閱者對象
  private subjects: { [key: string]: Function[] } = {};

  // 實作添加訂閱事件
  public on(subscriber: string, callback: () => void): void {
    console.log(`收到訂閱資訊,訂閱事件:${subscriber}`);
    if (!this.subjects[subscriber]) {
      this.subjects[subscriber] = [];
    }
    this.subjects[subscriber].push(callback);
  };

  // 實作取消訂閱事件
  public off(subscriber: string, callback: () => void): void {
    console.log(`收到取消訂閱請求,需要取消的訂閱事件:${subscriber}`);
    if (callback === null) {
      this.subjects[subscriber] = [];
    } else {
      const index: number = this.subjects[subscriber].indexOf(callback);
      ~index && this.subjects[subscriber].splice(index, 1);
    }
  };
  
  // 實作釋出訂閱事件
  public emit (subscriber: string, data = null): void {
    console.log(`收到釋出者資訊,執行訂閱事件:${subscriber}`);
    this.subjects[subscriber].forEach(item => item(data));
  };
}

class ConcretePublisher implements Publisher {
  public subscriber: string = "";
  public data: any; 
  constructor(subscriber: string, data: any) {
    this.subscriber = subscriber;
    this.data = data;
  }
}

class ConcreteSubscriber implements Subscriber {
  public subscriber: string = "";
  constructor(subscriber: string, callback: () => void) {
    this.subscriber = subscriber;
    this.callback = callback;
  }
  public callback(): void { };
}


/* 運作示例 */
const pingan8787 = new ConcreteSubscriber(
  "running",
  () => { 
    console.log("訂閱者 pingan8787 訂閱事件成功!執行回調~");
  }
);

const leo = new ConcreteSubscriber(
  "swimming",
  () => { 
    console.log("訂閱者 leo 訂閱事件成功!執行回調~");
  }
);

const lisa = new ConcreteSubscriber(
  "swimming",
  () => { 
    console.log("訂閱者 lisa 訂閱事件成功!執行回調~");
  }
);

const pual = new ConcretePublisher(
  "swimming",
  {message: "pual 釋出消息~"}
);

const eventBus = new ConcreteEventChannel();
eventBus.on(pingan8787.subscriber, pingan8787.callback);
eventBus.on(leo.subscriber, leo.callback);
eventBus.on(lisa.subscriber, lisa.callback);

// 釋出者 pual 釋出 "swimming"相關的事件
eventBus.emit(pual.subscriber, pual.data); 
eventBus.off (lisa.subscriber, lisa.callback);
eventBus.emit(pual.subscriber, pual.data);

/*
輸出結果:
[LOG]: 收到訂閱資訊,訂閱事件:running
[LOG]: 收到訂閱資訊,訂閱事件:swimming
[LOG]: 收到訂閱資訊,訂閱事件:swimming
[LOG]: 收到釋出者資訊,執行訂閱事件:swimming
[LOG]: 訂閱者 leo 訂閱事件成功!執行回調~ 
[LOG]: 訂閱者 lisa 訂閱事件成功!執行回調~ 
[LOG]: 收到取消訂閱請求,需要取消的訂閱事件:swimming
[LOG]: 收到釋出者資訊,執行訂閱事件:swimming
[LOG]: 訂閱者 leo 訂閱事件成功!執行回調~ 
*/      

2. Vue.js 使用示例

參考文章:《Vue事件總線(EventBus)使用詳細介紹》 (https://zhuanlan.zhihu.com/p/72777951)。

2.1 建立 event bus

在 Vue.js 中建立 EventBus 有兩種方式:

  1. 手動實作,導出 Vue 執行個體化的結果。
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue();      
  1. 直接在項目中的 ​

    ​main.js​

    ​全局挂載 Vue 執行個體化的結果。
// main.js
Vue.prototype.$EventBus = new Vue()      

2.2 發送事件

假設你有兩個Vue頁面需要通信:A 和 B ,A頁面按鈕上綁定了「點選事件」,發送一則消息,通知 B 頁面。

<!-- A.vue -->
<template>
    <button @click="sendMsg()">-</button>
</template>

<script>
import { EventBus } from "../event-bus.js";
export default {
  methods: {
    sendMsg() {
      EventBus.$emit("aMsg", '來自A頁面的消息');
    }
  }
};
</script>      

2.3 接收事件

B 頁面中接收消息,并展示内容到頁面上。

<!-- IncrementCount.vue -->
<template>
  <p>{{msg}}</p>
</template>

<script>
import {
  EventBus
} from "../event-bus.js";
export default {
  data(){
    return {
      msg: ''
    }
  },
  mounted() {
    EventBus.$on("aMsg", (msg) => {
      // A發送來的消息
      this.msg = msg;
    });
  }
};
</script>      

同理可以從 B 頁面往 A 頁面發送消息,使用下面方法:

// 發送消息
EventBus.$emit(channel: string, callback(payload1,…))

// 監聽接收消息
EventBus.$on(channel: string, callback(payload1,…))      

2.4 「移除事件監聽者」

使用 ​

​EventBus.$off('aMsg')​

​​ 來移除應用内所有對此某個事件的監聽。或者直接用 ​

​EventBus.$off()​

​ 來移除所有事件頻道,不需要添加任何參數 。

import { 
  eventBus 
} from './event-bus.js'
EventBus.$off('aMsg', {})      

六、總結

觀察者模式和釋出-訂閱模式的差别在于事件總線,如果有則是釋出-訂閱模式,反之為觀察者模式。是以在實作釋出-訂閱模式,關鍵在于實作這個事件總線,在某個特定時間觸發某個特定事件,進而觸發監聽這個特定事件的元件進行相應操作的功能。釋出-訂閱模式在很多時候非常有用。

參考文章

1.《釋出/訂閱》(https://zh.wikipedia.org/wiki/釋出/訂閱)  

2.《觀察者模式VS訂閱釋出模式》(https://molunerfinn.com/observer-vs-pubsub-pattern/#概述)

繼續閱讀