歡迎您光臨本站 註冊首頁

JavaScript設計模式之觀察者模式與發佈訂閱模式詳解

←手機掃碼閱讀     zmcjlove @ 2020-05-08 , reply:0

學習了一段時間設計模式,當學到觀察者模式和發佈訂閱模式的時候遇到了很大的問題,這兩個模式有點類似,有點傻傻分不清楚,博客起因如此,開始對觀察者和發佈訂閱開始了 Google 之旅。對整個學習過程做一個簡單的記錄。
觀察者模式
當對象間存在一對多關係時,則使用觀察者模式( Observer Pattern )。比如,當一個對象被修改時,則會自動通知它的依賴對象。觀察者模式屬於行為型模式。在觀察模式中共存在兩個角色 觀察者(Observer) 與 被觀察者(Subject) ,然而觀察者模式在軟件設計中是一個對象,維護一個依賴列表,當任何狀態發生改變自動通知它們。
其實觀察者模式是一個或多個觀察者對目標的狀態感興趣,它們通過將自己依附在目標對象之上以便註冊所感興趣的內容。目標狀態發生改變並且觀察者可能對這些改變感興趣,就會發送一個通知消息,調用每個觀察者的更新方法。當觀察者不再對目標狀態感興趣時,它們可以簡單的將自己從中分離。
在觀察者模式中一共分為這麼幾個角色:
Subject:維護一系列觀察者,方便添加或刪除觀察者 Observer:為那些在目標狀態發生改變時需要獲得通知的對象提供一個更新接口 ConcreteSuject:狀態發生改變時,想Observer發送通知,存儲ConcreteObserver的狀態 ConcreteObserver:具體的觀察者
舉例
舉一個生活中的例子,公司老闆可以為下面的工作人員分配認為,如果老闆作為被觀察者而存在,那麼下面所屬的那些員工則就作為觀察者而存在,為工作人員分配的任務來通知下面的工作人員應該去做哪些工作。
通過上面的例子可以對觀察者模式有一個簡單的認知,接下來結合下面的這張圖來再次分析一下上面的例子。
如果 Subject = 老闆 的話,那麼 Observer N = 工作人員 ,如果細心觀察的話會發現下圖中莫名到的多了一個 notify() ,那麼上述例子中的工作就是 notify() 。
既然各個關係已經屢清楚了,下面通過代碼來實現一下上述的例子:
// 觀察者隊列 class ObserverList{ constructor(){ this.observerList = {}; } Add(obj,type = "any"){ if(!this.observerList[type]){ this.observerList[type] = []; } this.observerList[type].push(obj); } Count(type = "any"){ return this.observerList[type].length; } Get(index,type = "any"){ let len = this.observerList[type].length; if(index > -1 && index < len){ return this.observerList[type][index] } } IndexOf(obj,startIndex,type = "any"){ let i = startIndex, pointer = -1; let len = this.observerList[type].length; while(i < len){ if(this.observerList[type][i] === obj){ pointer = i; } i++; } return pointer; } RemoveIndexAt(index,type = "any"){ let len = this.observerList[type].length; if(index === 0){ this.observerList[type].shift(); } else if(index === len-1){ this.observerList[type].pop(); } else{ this.observerList[type].splice(index,1); } } } // 老闆 class Boos { constructor(){ this.observers = new ObserverList(); } AddObserverList(observer,type){ this.observers.Add(observer,type); } RemoveObserver(oberver,type){ let i = this.observers.IndexOf(oberver,0,type); (i != -1) && this.observers.RemoveIndexAt(i,type); } Notify(type){ let oberverCont = this.observers.Count(type); for(let i=0;i<oberverCont;i++){ let emp = this.observers.Get(i,type); emp && emp(type); } } } class Employees { constructor(name){ this.name = name; } getName(){ return this.name; } } class Work { married(name){ console.log(`${name}上班`); } unemployment(name){ console.log(`${name}出差`); } writing(name){ console.log(`${name}寫作`); } writeCode(name){ console.log(`${name}打代碼`); } } let MyBoos = new Boos(); let work = new Work(); let aaron = new Employees("Aaron"); let angie = new Employees("Angie"); let aaronName = aaron.getName(); let angieName = angie.getName(); MyBoos.AddObserverList(work.married,aaronName); MyBoos.AddObserverList(work.writeCode,aaronName); MyBoos.AddObserverList(work.writing,aaronName); MyBoos.RemoveObserver(work.writing,aaronName); MyBoos.Notify(aaronName); MyBoos.AddObserverList(work.married,angieName); MyBoos.AddObserverList(work.unemployment,angieName); MyBoos.Notify(angieName); // Aaron上班 // Aaron打代碼 // Angie上班 // Angie出差
代碼裡面完全遵循了流程圖, Boos 類作為被觀察者而存在, Staff 作為觀察者,通過 Work 兩者做關聯。
如果相信的閱讀上述代碼的話可以出,其實觀察者的核心代碼就是 peopleList 這個對象,這個對象裡面存放了 N 多個 Array 數組,通過 run 方法觸發觀察者的 notify 隊列。觀察者模式主要解決的問題就是,一個對象狀態改變給其他對象通知的問題,而且要考慮到易用和低耦合,保證高度的協作。當我們在做程序設計的時候,當一個目標對象的狀態發生改變,所有的觀察者對象都將得到通知,進行廣播通知的時候,就可以使用觀察者模式啦。
優點
觀察者和被觀察者是抽象耦合的。 建立一套觸發機制。
缺點
如果一個被觀察者對象有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。 如果在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間進行循環調用,可能導致系統崩潰。 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
小結
對於觀察者模式在被觀察者中有一個用於存儲觀察者對象的 list 隊列,通過統一的方法觸發,目標和觀察者是基類,目標提供維護觀察者的一系列方法,觀察者提供更新接口。具體觀察者和具體目標繼承各自的基類,然後具體觀察者把自己註冊到具體目標裡,在具體目標發生變化時候,調度觀察者的更新方法。
發佈/訂閱模式
在發佈訂閱模式上卡了很久,但是廢了好長時間沒有搞明白,也不知道自己的疑問在哪,於是就瘋狂 Google 不斷地翻閱找到自己的疑問,個人覺得如果想要搞明白髮布訂閱模式首先要搞明白誰是發佈者,誰是訂閱者。
發佈訂閱:在軟件架構中,發佈-訂閱是一種消息範式,消息的發送者(稱為發佈者)不會將消息直接發送給特定的接收者(稱為訂閱者)。而是將發佈的消息分為不同的類別,無需瞭解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達對一個或多個類別的興趣,只接收感興趣的消息,無需瞭解哪些發佈者(如果有的話)存在。-- 維基百科
看了半天沒整明白(✿◡‿◡),慚愧...於是,學習的路途不能止步,繼續...
大概很多人都和我一樣,覺得發佈訂閱模式裡的 Publisher ,就是觀察者模式裡的 Subject ,而 Subscriber ,就是 Observer 。 Publisher 變化時,就主動去通知 Subscriber 。其實並不是。在發佈訂閱模式裡,發佈者,並不會直接通知訂閱者,換句話說,發佈者和訂閱者,彼此互不相識。互不相識?那他們之間如何交流?
答案是,通過第三者,也就是在消息隊列裡面,我們常說的經紀人 Broker 。
發佈者只需告訴 Broker ,我要發的消息, topic 是 AAA ,訂閱者只需告訴 Broker ,我要訂閱 topic 是 AAA 的消息,於是,當 Broker 收到發佈者發過來消息,並且 topic 是 AAA 時,就會把消息推送給訂閱了 topic 是 AAA 的訂閱者。當然也有可能是訂閱者自己過來拉取,看具體實現。
也就是說,發佈訂閱模式裡,發佈者和訂閱者,不是鬆耦合,而是完全解耦的。
通過上面的描述終於有了一些眉目,再舉一個生活中的例子,就拿微信公眾號來說,每次微信公眾號推送消息並不是一下子推送給微信的所有用戶,而是選擇性的推送給那些已經訂閱了該公眾號的人。
老規矩吧,用代碼實現一下:
class Utils { constructor(){ this.observerList = {}; } Add(obj,type = "any"){ if(!this.observerList[type]){ this.observerList[type] = []; } this.observerList[type].push(obj); } Count(type = "any"){ return this.observerList[type].length; } Get(index,type = "any"){ let len = this.observerList[type].length; if(index > -1 && index < len){ return this.observerList[type][index] } } IndexOf(obj,startIndex,type = "any"){ let i = startIndex, pointer = -1; let len = this.observerList[type].length; while(i < len){ if(this.observerList[type][i] === obj){ pointer = i; } i++; } return pointer; } } // 訂閱者 class Subscribe extends Utils {}; // 發佈者 class Publish extends Utils {}; // 中轉站 class Broker { constructor(){ this.publish = new Publish(); this.subscribe = new Subscribe(); } // 訂閱 Subscribe(fn,key){ this.subscribe.Add(fn,key); } // 發佈 Release(fn,key){ this.publish.Add(fn,key); } Run(key = "any"){ let publishList = this.publish.observerList; let subscribeList = this.subscribe.observerList; if(!publishList[key] || !subscribeList[key]) throw "No subscribers or published messages"; let pub = publishList[key]; let sub = subscribeList[key]; let arr = [...pub,...sub]; while(arr.length){ let item = arr.shift(); item(key); } } } class Employees { constructor(name){ this.name = name; } getName(){ let {name} = this; return name; } receivedMessage(key,name){ console.log(`${name}收到了${key}發來的消息`); } } class Public { constructor(name){ this.name = name; } getName(){ let {name} = this; return name; } sendMessage(key){ console.log(`${key}發送了一條消息`); } } let broker = new Broker(); let SundayPublic = new Public("Sunday"); let MayPublic = new Public("May"); let Angie = new Employees("Angie"); let Aaron = new Employees("Aaron"); broker.Subscribe(() => { Angie.receivedMessage(SundayPublic.getName(),Angie.getName()); },SundayPublic.getName()); broker.Subscribe(() => { Angie.receivedMessage(SundayPublic.getName(),Aaron.getName()); },SundayPublic.getName()); broker.Subscribe(() => { Aaron.receivedMessage(MayPublic.getName(),Aaron.getName()); },MayPublic.getName()); broker.Release(MayPublic.sendMessage,MayPublic.getName()); broker.Release(SundayPublic.sendMessage,SundayPublic.getName()); broker.Run(SundayPublic.getName()); broker.Run(MayPublic.getName()); // Sunday發送了一條消息 // Angie收到了Sunday發來的消息 // Aaron收到了Sunday發來的消息 // May發送了一條消息 // Aaron收到了May發來的消息
通過上面的輸出結果可以得出,只要訂閱過該公眾號的用戶,只要公眾號發送一條消息,所有訂閱過該條消息的用戶都是可以收到這條消息。雖然代碼有點多,但是確確實實能夠體現發佈訂閱模式的魅力,很不錯。
優點
支持簡單的廣播通信,當對象狀態發生改變時,會自動通知已經訂閱過的對象。 發佈者與訂閱者耦合性降低,發佈者只管發佈一條消息出去,它不關心這條消息如何被訂閱者使用,同時,訂閱者只監聽發佈者的事件名,只要發佈者的事件名不變,它不管發佈者如何改變;同理賣家(發佈者)它只需要將鞋子來貨的這件事告訴訂閱者(買家),他不管買家到底買還是不買,還是買其他賣家的。只要鞋子到貨了就通知訂閱者即可。
缺點
創建訂閱者需要消耗一定的時間和內存。 雖然可以弱化對象之間的聯繫,如果過度使用的話,反而使代碼不好理解及代碼不好維護。
小結
發佈訂閱模式可以降低發佈者與訂閱者之間的耦合程度,兩者之間從來不關係你是誰,你要作什麼?訂閱者只需要跟隨發佈者,若發佈者發生變化就會通知訂閱者應該也做出相對於的變化。發佈者與訂閱者之間不存在直接通信,他們所有的一切事情都是通過中介者相互通信,它過濾所有傳入的消息並相應地分發它們。發佈訂閱模式可用於在不同系統組件之間傳遞消息的模式,而這些組件不知道關於彼此身份的任何信息。
觀察者模式與發佈訂閱的區別
在 Observer 模式中, Observers 知道 Subject ,同時 Subject 還保留了 Observers 的記錄。然而,在發佈者/訂閱者中,發佈者和訂閱者不需要彼此瞭解。他們只是在消息隊列或代理的幫助下進行通信。 在 Publisher / Subscriber 模式中,組件是鬆散耦合的,而不是 Observer 模式。 觀察者模式主要以同步方式實現,即當某些事件發生時, Subject 調用其所有觀察者的適當方法。發佈者/訂閱者在大多情況下是異步方式(使用消息隊列)。 觀察者模式需要在單個應用程序地址空間中實現。另一方面,發佈者/訂閱者模式更像是跨應用程序模式。
如果以結構來分辨模式,發佈訂閱模式相比觀察者模式多了一箇中間件訂閱器,所以發佈訂閱模式是不同於觀察者模式的。如果以意圖來分辨模式,他們都是實現了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知,並自動更新,那麼他們就是同一種模式,發佈訂閱模式是在觀察者模式的基礎上做的優化升級。在觀察者模式中,觀察者需要直接訂閱目標事件。在目標發出內容改變的事件後,直接接收事件並作出響應。發佈訂閱模式相比觀察者模式多了個事件通道,訂閱者和發佈者不是直接關聯的。目標和觀察者是直接聯繫在一起的。觀察者把自身添加到了目標對象中,可見和發佈訂閱模式差別還是很大的。在這種模式下,目標更像一個發佈者,他讓添加進來的所有觀察者都執行了傳入的函數,而觀察者就像一個訂閱者。雖然兩種模式都存在訂閱者和發佈者(具體觀察者可認為是訂閱者、具體目標可認為是發佈者),但是觀察者模式是由具體目標調度的,而發佈/訂閱模式是統一由調度中心調的,所以觀察者模式的訂閱者與發佈者之間是存在依賴的,而發佈/訂閱模式則不會。
總結
雖然在學習這兩種模式的時候有很多的坎坷,最終還是按照自己的理解寫出來了兩個案例。或許理解的有偏差,如果哪裡有問題,希望大家在下面留言指正,我會盡快做出修復的。


[zmcjlove ] JavaScript設計模式之觀察者模式與發佈訂閱模式詳解已經有264次圍觀

http://coctec.com/docs/javascript/show-post-233289.html