發佈訂閱模式 前一篇對觀察者模式做了介紹,重點在於觀察者和被觀察者的對應關係,以及將被觀察者的改變及時通知到相對應的觀察者。 這樣的模式基本上可以解決少量數據源的情景,在觀察者和被觀察者可能是多對多關係的情況下,強耦合的結構會讓代碼不夠清晰,難以維護。 在《JavaScript設計模式》一書中,提到 ...
發佈訂閱模式
前一篇對觀察者模式做了介紹,重點在於觀察者和被觀察者的對應關係,以及將被觀察者的改變及時通知到相對應的觀察者。
這樣的模式基本上可以解決少量數據源的情景,在觀察者和被觀察者可能是多對多關係的情況下,強耦合的結構會讓代碼不夠清晰,難以維護。
在《JavaScript設計模式》一書中,提到了Observer和Publish/Subscribe的區別。
Observer模式要求希望接收到主題同志的觀察者(或對象)必須訂閱內容改變的事件。
Publish/Subscribe模式使用了一個主題/事件通道,這個通道介於希望接收到通知(訂閱者)的對象和激活事件的對象(發佈者)之間。該事件系統允許代碼定義應用程式的特定事件,這些事件可以傳遞自定義參數,自定義參數包含訂閱者所需的值。其目的是避免訂閱者和發佈者之間產生依賴關係。
這裡的關鍵點在於,通過一個事件中心,將發佈者和訂閱者的耦合關係解開,發佈者和訂閱者通過事件中心來產生聯繫。
打個比方,發佈者像是發佈小廣告的,事件中心是一個調度站,訂閱者則告訴事件中心,我關註A、B類型的廣告,如果有更新,請通知我。調度站記錄A,B類型下的訂閱者,等到A,B廣告發佈時,通知到訂閱者。
這個例子里,發佈者不關心訂閱者是誰,也不維護訂閱者列表,同訂閱者解耦,只將自己發佈的內容提交到事件中心。而訂閱者和主題的關係,交給了事件中心來維護。
畫一個類圖來解釋一下他們的關係。
先來定義兩個虛類:Publisher 和 Subscriber。
abstract class Publisher {
data: string;
id: string;
abstract publish(any);
}
abstract class Subscriber {
id: string;
abstract subscribe(topicId: string);
abstract update(topicData: string);
}
接著來繼承這兩個類,聲明兩個實體類:
class ApplePublisher extends Publisher {
private _data;
private _id;
private channel;
constructor (defaultId, defaultData, defaultChannel: TopicChannel) {
super();
this._id = defaultId;
this._data = defaultData;
this.channel = defaultChannel;
}
get id () {
return this._id;
}
set id (newId) {
this._id = newId;
}
get data () {
return this._data;
}
set data (newData) {
this._data = newData;
}
publish () {
this.channel.publishBaseId(this.id);
}
}
class FruitSubscriber extends Subscriber {
readonly _id;
readonly _publishId;
private channel;
constructor (id: string, publishId: string, topicChannel: TopicChannel) {
super();
this._id = id;
this._publishId = publishId;
this.channel = topicChannel;
this.subscribe(this._publishId);
}
subscribe (topicId: string) {
this.channel.subscribe(topicId, this);
}
update (topicData: string) {
console.log('fruit subscriber ' + this._id + ': ' + topicData);
}
}
最後來實現TopicChannel,這個類用來處理訂閱、發佈通知功能。
class TopicChannel {
private publisherMap;
private subscriberMap;
constructor () {
this.publisherMap = new Map<string, Publisher>();
this.subscriberMap = new Map<string, Array<Subscriber>>();
}
addPublisher (publisher: Publisher) {
this.publisherMap.set(publisher.id, publisher);
}
removePublisher (publisher: Publisher) {
this.publisherMap.delete(publisher.id)
}
clearPublisher () {
this.publisherMap.clear();
}
subscribe (publisherId: string, subscriber: Subscriber) {
if (this.subscriberMap.has(publisherId)) {
this.subscriberMap.get(publisherId).push(subscriber);
} else {
this.subscriberMap.set(publisherId, [subscriber]);
}
}
publishBaseId (publisherId: string) {
if (this.publisherMap.has(publisherId)) {
this.subscriberMap.get(publisherId).forEach((item)=>{
item.update(this.publisherMap.get(publisherId).data);
})
} else {
console.log('There is not the publisher!');
}
}
}
TopicChannel通過TopicId來維護髮布者和訂閱者的關係,使得發佈者和訂閱者充分解耦。
使得訂閱者可以訂閱多個主題,在內部根據主題的不同,執行不同的邏輯。
發佈者則完全無視訂閱者的邏輯,只管將自己的內容推送到TopicChannel。
let topicChannel = new TopicChannel();
let applePublisher1 = new ApplePublisher('apple publisher1', 'foo apple1', topicChannel);
let applePublisher2 = new ApplePublisher('apple publisher2', 'foo apple2', topicChannel);
topicChannel.addPublisher(applePublisher1);
topicChannel.addPublisher(applePublisher2);
let fruitSubscriber1 = new FruitSubscriber('fruit1', 'apple publisher1', topicChannel);
let fruitSubscriber2 = new FruitSubscriber('fruit2', 'apple publisher2', topicChannel);
let fruitSubscriber3 = new FruitSubscriber('fruit3', 'apple publisher1', topicChannel);
fruitSubscriber2.subscribe('apple publisher1');
applePublisher1.publish();
applePublisher2.publish();
結果為:
fruit subscriber fruit1: foo apple1
fruit subscriber fruit3: foo apple1
fruit subscriber fruit2: foo apple1
fruit subscriber fruit2: foo apple2
發佈訂閱模式將發佈者和訂閱者完全解耦,由事件中心通過topicId或者是其他唯一key來維護二者的關係,使得程式可以分割成更小,內聚更高的模塊。
與此同時,由於弱化了發佈者與訂閱者的關係,使得發佈者難以追蹤到訂閱者,無法獲得來自訂閱者反饋;並且同一主題的訂閱者之間相對透明,不能產生聯動。
以上,如有錯誤,敬請指正,感謝閱讀。