【JS】638- 發佈訂閱模式與觀察者模式

作者:hfhan

https://segmentfault.com/a/1190000018706349

背景

設計模式並非是軟件開發的專業術語,實際上,“模式”最早誕生於建築學。

設計模式的定義是:在面向對象軟件設計過程中針對特定問題的簡潔而優雅的解決方案。通俗一點說,設計模式是在某種場合下對某個問題的一種解決方案。如果再通俗一點說,設計模式就是給面向對象軟件開發中的一些好的設計取個名字。

這些“好的設計”並不是誰發明的,而是早已存在於軟件開發中。一個稍有經驗的程序員也許在不知不覺中數次使用過這些設計模式。GoF(Gang of Four--四人組,《設計模式》幾位作者)最大的功績是把這些“好的設計”從浩瀚的面向對象世界中挑選出來,並且給予它們一個好聽又好記的名字。

設計模式並不直接用來完成代碼的編寫,而是描述在各種不同情況下,要怎麼解決問題的一種方案,他不是一個死的機制,他是一種思想,一種寫代碼的形式。每種語言對於各種設計模式都有他們自己的實現方式,對於某些設計模式來說,可能在某些語言下並不適用,比如工廠方法模式對於javascript。模式應該用在正確的地方。而哪些纔算正確的地方,只有在我們深刻理解了模式的意圖之後,再結合項目的實際場景纔會知道。。

模式的社區一直在發展。GoF在1995年提出了23種設計模式,但模式不僅僅侷限於這23種,後面增加到了24種。在這20多年的時間裏,也許有更多的模式已經被人發現並總結了出來,比如一些JavaScript 圖書中會提到模塊模式、沙箱模式等。這些“模式”能否被世人公認並流傳下來,還有待時間驗證。

觀察者模式(Observer Pattern)

觀察者模式定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知,並自動更新。觀察者模式屬於行爲型模式,行爲型模式關注的是對象之間的通訊,觀察者模式就是觀察者和被觀察者之間的通訊。

觀察者模式有一個別名叫“發佈-訂閱模式”,或者說是“訂閱-發佈模式”,訂閱者和訂閱目標是聯繫在一起的,當訂閱目標發生改變時,逐個通知訂閱者。我們可以用報紙期刊的訂閱來形象的說明,當你訂閱了一份報紙,每天都會有一份最新的報紙送到你手上,有多少人訂閱報紙,報社就會發多少份報紙,報社和訂報紙的客戶就是上面文章開頭所說的“一對多”的依賴關係。

發佈訂閱模式(Pub-Sub Pattern)

其實24種基本的設計模式中並沒有發佈訂閱模式,上面也說了,他只是觀察者模式的一個別稱。

但是經過時間的沉澱,似乎他已經強大了起來,已經獨立於觀察者模式,成爲另外一種不同的設計模式。

在現在的發佈訂閱模式中,稱爲發佈者的消息發送者不會將消息直接發送給訂閱者,這意味着發佈者和訂閱者不知道彼此的存在。在發佈者和訂閱者之間存在第三個組件,稱爲消息代理或調度中心或中間件,它維持着發佈者和訂閱者之間的聯繫,過濾所有發佈者傳入的消息並相應地分發它們給訂閱者。

舉一個例子,你在微博上關注了A,同時其他很多人也關注了A,那麼當A發佈動態的時候,微博就會爲你們推送這條動態。A就是發佈者,你是訂閱者,微博就是調度中心,你和A是沒有直接的消息往來的,全是通過微博來協調的(你的關注,A的發佈動態)。

觀察者模式和發佈訂閱模式有什麼區別?

我們先來看下這兩個模式的實現結構:

觀察者模式:觀察者(Observer)直接訂閱(Subscribe)主題(Subject),而當主題被激活的時候,會觸發(Fire Event)觀察者裏的事件。

發佈訂閱模式:訂閱者(Subscriber)把自己想訂閱的事件註冊(Subscribe)到調度中心(Topic),當發佈者(Publisher)發佈該事件(Publish topic)到調度中心,也就是該事件觸發時,由調度中心統一調度(Fire Event)訂閱者註冊到調度中心的處理代碼。

我們再來看下這兩個模式的代碼案例:(獵人發佈與訂閱任務)

觀察者模式:

    //有一家獵人工會,其中每個獵人都具有發佈任務(publish),訂閱任務(subscribe)的功能
    //他們都有一個訂閱列表來記錄誰訂閱了自己
    //定義一個獵人類
    //包括姓名,級別,訂閱列表
    function Hunter(name, level){
        this.name = name
        this.level = level
        this.list = []
    }
    Hunter.prototype.publish = function (money){
        console.log(this.level + '獵人' + this.name + '尋求幫助')
        this.list.forEach(function(item, index){
            item(money)
        })
    }
    Hunter.prototype.subscribe = function (targrt, fn){
        console.log(this.level + '獵人' + this.name + '訂閱了' + targrt.name)
        targrt.list.push(fn)
    }
    //獵人工會走來了幾個獵人
    let hunterMing = new Hunter('小明', '黃金')
    let hunterJin = new Hunter('小金', '白銀')
    let hunterZhang = new Hunter('小張', '黃金')
    let hunterPeter = new Hunter('Peter', '青銅')
    //Peter等級較低,可能需要幫助,所以小明,小金,小張都訂閱了Peter
    hunterMing.subscribe(hunterPeter, function(money){
        console.log('小明表示:' + (money > 200 ? '' : '暫時很忙,不能') + '給予幫助')
    })
    hunterJin.subscribe(hunterPeter, function(){
        console.log('小金表示:給予幫助')
    })
    hunterZhang.subscribe(hunterPeter, function(){
        console.log('小張表示:給予幫助')
    })
    //Peter遇到困難,賞金198尋求幫助
    hunterPeter.publish(198)
    //獵人們(觀察者)關聯他們感興趣的獵人(目標對象),如Peter,當Peter有困難時,會自動通知給他們(觀察者)

發佈訂閱模式:

    //定義一家獵人工會
    //主要功能包括任務發佈大廳(topics),以及訂閱任務(subscribe),發佈任務(publish)
    let HunterUnion = {
        type: 'hunt',
        topics: Object.create(null),
        subscribe: function (topic, fn){
            if(!this.topics[topic]){
                  this.topics[topic] = [];  
            }
            this.topics[topic].push(fn);
        },
        publish: function (topic, money){
            if(!this.topics[topic])
                  return;
            for(let fn of this.topics[topic]){
                fn(money)
            }
        }
    }
    //定義一個獵人類
    //包括姓名,級別
    function Hunter(name, level){
        this.name = name
        this.level = level
    }
    //獵人可在獵人工會發布訂閱任務
    Hunter.prototype.subscribe = function (topic, fn){
        console.log(this.level + '獵人' + this.name + '訂閱了狩獵' + topic + '的任務')
        HunterUnion.subscribe(topic, fn)
    }
    Hunter.prototype.publish = function (topic, money){
        console.log(this.level + '獵人' + this.name + '發佈了狩獵' + topic + '的任務')
        HunterUnion.publish(topic, money)
    }
    //獵人工會走來了幾個獵人
    let hunterMing = new Hunter('小明', '黃金')
    let hunterJin = new Hunter('小金', '白銀')
    let hunterZhang = new Hunter('小張', '黃金')
    let hunterPeter = new Hunter('Peter', '青銅')
    //小明,小金,小張分別訂閱了狩獵tiger的任務
    hunterMing.subscribe('tiger', function(money){
        console.log('小明表示:' + (money > 200 ? '' : '不') + '接取任務')
    })
    hunterJin.subscribe('tiger', function(money){
        console.log('小金表示:接取任務')
    })
    hunterZhang.subscribe('tiger', function(money){
        console.log('小張表示:接取任務')
    })
    //Peter訂閱了狩獵sheep的任務
    hunterPeter.subscribe('sheep', function(money){
        console.log('Peter表示:接取任務')
    })
    //Peter發佈了狩獵tiger的任務
    hunterPeter.publish('tiger', 198)
    //獵人們發佈(發佈者)或訂閱(觀察者/訂閱者)任務都是通過獵人工會(調度中心)關聯起來的,他們沒有直接的交流。

觀察者模式和發佈訂閱模式最大的區別就是發佈訂閱模式有個事件調度中心。

觀察者模式由具體目標調度,每個被訂閱的目標裏面都需要有對觀察者的處理,這種處理方式比較直接粗暴,但是會造成代碼的冗餘。

而發佈訂閱模式中統一由調度中心進行處理,訂閱者和發佈者互不干擾,消除了發佈者和訂閱者之間的依賴。這樣一方面實現瞭解耦,還有就是可以實現更細粒度的一些控制。比如發佈者發佈了很多消息,但是不想所有的訂閱者都接收到,就可以在調度中心做一些處理,類似於權限控制之類的。還可以做一些節流操作。

觀察者模式是不是發佈訂閱模式

網上關於這個問題的回答,出現了兩極分化,有認爲發佈訂閱模式就是觀察者模式的,也有認爲觀察者模式和發佈訂閱模式是真不一樣的。

其實我不知道發佈訂閱模式是不是觀察者模式,就像我不知道辨別模式的關鍵是設計意圖還是設計結構(理念),雖然《JavaScript設計模式與開發實踐》一書中說了 分辨模式的關鍵是意圖而不是結構

如果以結構來分辨模式,發佈訂閱模式相比觀察者模式多了一箇中間件訂閱器,所以發佈訂閱模式是不同於觀察者模式的;如果以意圖來分辨模式,他們都是 實現了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知,並自動更新,那麼他們就是同一種模式,發佈訂閱模式是在觀察者模式的基礎上做的優化升級。

不過,不管他們是不是同一個設計模式,他們的實現方式確實有差別,我們在使用的時候應該根據場景來判斷選擇哪個。

1. JavaScript 重溫系列(22篇全)

2. ECMAScript 重溫系列(10篇全)

3. JavaScript設計模式 重溫系列(9篇全)

4. 正則 / 框架 / 算法等 重溫系列(16篇全)

5. Webpack4 入門(上)|| Webpack4 入門(下)

6. MobX 入門(上) ||  MobX 入門(下)

7. 59篇原創系列彙總

回覆“加羣”與大佬們一起交流學習~

點擊“閱讀原文”查看70+篇原創文章

點這,與大家一起分享本文吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章