1. 程式人生 > 程式設計 >JavaScript設計模式之觀察者模式與釋出訂閱模式詳解

JavaScript設計模式之觀察者模式與釋出訂閱模式詳解

本文例項講述了JavaScript設計模式之觀察者模式與釋出訂閱模式。分享給大家供大家參考,具體如下:

學習了一段時間設計模式,當學到觀察者模式和釋出訂閱模式的時候遇到了很大的問題,這兩個模式有點類似,有點傻傻分不清楚,部落格起因如此,開始對觀察者和釋出訂閱開始了Google之旅。對整個學習過程做一個簡單的記錄。

觀察者模式

當物件間存在一對多關係時,則使用觀察者模式(Observer Pattern)。比如,當一個物件被修改時,則會自動通知它的依賴物件。觀察者模式屬於行為型模式。在觀察模式中共存在兩個角色觀察者(Observer)被觀察者(Subject),然而觀察者模式在軟體設計中是一個物件,維護一個依賴列表,當任何狀態發生改變自動通知它們。

其實觀察者模式是一個或多個觀察者對目標的狀態感興趣,它們通過將自己依附在目標物件之上以便註冊所感興趣的內容。目標狀態發生改變並且觀察者可能對這些改變感興趣,就會發送一個通知訊息,呼叫每個觀察者的更新方法。當觀察者不再對目標狀態感興趣時,它們可以簡單的將自己從中分離。

在觀察者模式中一共分為這麼幾個角色:

  1. Subject:維護一系列觀察者,方便新增或刪除觀察者
  2. Observer:為那些在目標狀態發生改變時需要獲得通知的物件提供一個更新介面
  3. ConcreteSuject:狀態發生改變時,想Observer傳送通知,儲存ConcreteObserver的狀態
  4. ConcreteObserver:具體的觀察者
舉例

舉一個生活中的例子,公司老闆可以為下面的工作人員分配認為,如果老闆作為被觀察者而存在,那麼下面所屬的那些員工則就作為觀察者而存在,為工作人員分配的任務來通知下面的工作人員應該去做哪些工作。

通過上面的例子可以對觀察者模式有一個簡單的認知,接下來結合下面的這張圖來再次分析一下上面的例子。

如果Subject = 老闆的話,那麼Observer N = 工作人員,如果細心觀察的話會發現下圖中莫名到的多了一個notify(),那麼上述例子中的工作就是notify()

JavaScript設計模式之觀察者模式與釋出訂閱模式詳解

既然各個關係已經屢清楚了,下面通過程式碼來實現一下上述的例子:

// 觀察者佇列
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,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佇列。觀察者模式主要解決的問題就是,一個物件狀態改變給其他物件通知的問題,而且要考慮到易用和低耦合,保證高度的協作。當我們在做程式設計的時候,當一個目標物件的狀態發生改變,所有的觀察者物件都將得到通知,進行廣播通知的時候,就可以使用觀察者模式啦。

優點
  1. 觀察者和被觀察者是抽象耦合的。
  2. 建立一套觸發機制。
缺點
  1. 如果一個被觀察者物件有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。
  2. 如果在觀察者和觀察目標之間有迴圈依賴的話,觀察目標會觸發它們之間進行迴圈呼叫,可能導致系統崩潰。
  3. 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標物件是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
小結

對於觀察者模式在被觀察者中有一個用於儲存觀察者物件的list佇列,通過統一的方法觸發,目標和觀察者是基類,目標提供維護觀察者的一系列方法,觀察者提供更新介面。具體觀察者和具體目標繼承各自的基類,然後具體觀察者把自己註冊到具體目標裡,在具體目標發生變化時候,排程觀察者的更新方法。

釋出/訂閱模式

在釋出訂閱模式上卡了很久,但是廢了好長時間沒有搞明白,也不知道自己的疑問在哪,於是就瘋狂Google不斷地翻閱找到自己的疑問,個人覺得如果想要搞明白髮布訂閱模式首先要搞明白誰是釋出者,誰是訂閱者。

釋出訂閱:在軟體架構中,釋出-訂閱是一種訊息正規化,訊息的傳送者(稱為釋出者)不會將訊息直接傳送給特定的接收者(稱為訂閱者)。而是將釋出的訊息分為不同的類別,無需瞭解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達對一個或多個類別的興趣,只接收感興趣的訊息,無需瞭解哪些釋出者(如果有的話)存在。-- 維基百科

看了半天沒整明白(✿◡‿◡),慚愧...於是,學習的路途不能止步,繼續...

大概很多人都和我一樣,覺得釋出訂閱模式裡的Publisher,就是觀察者模式裡的Subject,而Subscriber,就是ObserverPublisher變化時,就主動去通知Subscriber。其實並不是。在釋出訂閱模式裡,釋出者,並不會直接通知訂閱者,換句話說,釋出者和訂閱者,彼此互不相識。互不相識?那他們之間如何交流?

答案是,通過第三者,也就是在訊息佇列裡面,我們常說的經紀人Broker

釋出者只需告訴Broker,我要發的訊息,topicAAA,訂閱者只需告訴Broker,我要訂閱topicAAA的訊息,於是,當Broker收到釋出者發過來訊息,並且topicAAA時,就會把訊息推送給訂閱了topicAAA的訂閱者。當然也有可能是訂閱者自己過來拉取,看具體實現。

也就是說,釋出訂閱模式裡,釋出者和訂閱者,不是鬆耦合,而是完全解耦的。

JavaScript設計模式之觀察者模式與釋出訂閱模式詳解

通過上面的描述終於有了一些眉目,再舉一個生活中的例子,就拿微信公眾號來說,每次微信公眾號推送訊息並不是一下子推送給微信的所有使用者,而是選擇性的推送給那些已經訂閱了該公眾號的人。

老規矩吧,用程式碼實現一下:

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,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(),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發來的訊息

通過上面的輸出結果可以得出,只要訂閱過該公眾號的使用者,只要公眾號傳送一條訊息,所有訂閱過該條訊息的使用者都是可以收到這條訊息。雖然程式碼有點多,但是確確實實能夠體現釋出訂閱模式的魅力,很不錯。

優點
  1. 支援簡單的廣播通訊,當物件狀態發生改變時,會自動通知已經訂閱過的物件。
  2. 釋出者與訂閱者耦合性降低,釋出者只管釋出一條訊息出去,它不關心這條訊息如何被訂閱者使用,同時,訂閱者只監聽釋出者的事件名,只要釋出者的事件名不變,它不管釋出者如何改變;同理賣家(釋出者)它只需要將鞋子來貨的這件事告訴訂閱者(買家),他不管買家到底買還是不買,還是買其他賣家的。只要鞋子到貨了就通知訂閱者即可。
缺點
  1. 建立訂閱者需要消耗一定的時間和記憶體。
  2. 雖然可以弱化物件之間的聯絡,如果過度使用的話,反而使程式碼不好理解及程式碼不好維護。
小結

釋出訂閱模式可以降低釋出者與訂閱者之間的耦合程度,兩者之間從來不關係你是誰,你要作什麼?訂閱者只需要跟隨釋出者,若釋出者發生變化就會通知訂閱者應該也做出相對於的變化。釋出者與訂閱者之間不存在直接通訊,他們所有的一切事情都是通過中介者相互通訊,它過濾所有傳入的訊息並相應地分發它們。釋出訂閱模式可用於在不同系統元件之間傳遞訊息的模式,而這些元件不知道關於彼此身份的任何資訊。

觀察者模式與釋出訂閱的區別

  1. Observer模式中,Observers知道Subject,同時Subject還保留了Observers的記錄。然而,在釋出者/訂閱者中,釋出者和訂閱者不需要彼此瞭解。他們只是在訊息佇列或代理的幫助下進行通訊。
  2. Publisher / Subscriber模式中,元件是鬆散耦合的,而不是Observer模式。
  3. 觀察者模式主要以同步方式實現,即當某些事件發生時,Subject呼叫其所有觀察者的適當方法。釋出者/訂閱者在大多情況下是非同步方式(使用訊息佇列)。
  4. 觀察者模式需要在單個應用程式地址空間中實現。另一方面,釋出者/訂閱者模式更像是跨應用程式模式。

JavaScript設計模式之觀察者模式與釋出訂閱模式詳解

如果以結構來分辨模式,釋出訂閱模式相比觀察者模式多了一箇中間件訂閱器,所以釋出訂閱模式是不同於觀察者模式的。如果以意圖來分辨模式,他們都是實現了物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知,並自動更新,那麼他們就是同一種模式,釋出訂閱模式是在觀察者模式的基礎上做的優化升級。在觀察者模式中,觀察者需要直接訂閱目標事件。在目標發出內容改變的事件後,直接接收事件並作出響應。釋出訂閱模式相比觀察者模式多了個事件通道,訂閱者和釋出者不是直接關聯的。目標和觀察者是直接聯絡在一起的。觀察者把自身新增到了目標物件中,可見和釋出訂閱模式差別還是很大的。在這種模式下,目標更像一個釋出者,他讓新增進來的所有觀察者都執行了傳入的函式,而觀察者就像一個訂閱者。雖然兩種模式都存在訂閱者和釋出者(具體觀察者可認為是訂閱者、具體目標可認為是釋出者),但是觀察者模式是由具體目標排程的,而釋出/訂閱模式是統一由排程中心調的,所以觀察者模式的訂閱者與釋出者之間是存在依賴的,而釋出/訂閱模式則不會。

總結

雖然在學習這兩種模式的時候有很多的坎坷,最終還是按照自己的理解寫出來了兩個案例。或許理解的有偏差,如果哪裡有問題,希望大家在下面留言指正,我會盡快做出修復的。

感興趣的朋友可以使用線上HTML/CSS/JavaScript程式碼執行工具:http://tools.jb51.net/code/HtmlJsRun測試上述程式碼執行效果。

更多關於JavaScript相關內容感興趣的讀者可檢視本站專題:《javascript面向物件入門教程》、《JavaScript錯誤與除錯技巧總結》、《JavaScript資料結構與演算法技巧總結》、《JavaScript遍歷演算法與技巧總結》及《JavaScript數學運算用法總結》

希望本文所述對大家JavaScript程式設計有所幫助。