觀察者模式介紹

邢闖洋發表於2021-12-03

亦稱: 事件訂閱者、監聽者、Event-Subscriber、Listener、Observer

意圖

觀察者模式是一種行為設計模式, 允許你定義一種訂閱機制, 可在物件事件發生時通知多個 “觀察” 該物件的其他物件。

觀察者設計模式

問題

假如你有兩種型別的物件: ​ 顧客商店 。 顧客對某個特定品牌的產品非常感興趣 (例如最新型號的 iPhone 手機), 而該產品很快將會在商店裡出售。

顧客可以每天來商店看看產品是否到貨。 但如果商品尚未到貨時, 絕大多數來到商店的顧客都會空手而歸。

訪問商店或傳送垃圾郵件

前往商店和傳送垃圾郵件

另一方面, 每次新產品到貨時, 商店可以向所有顧客傳送郵件 (可能會被視為垃圾郵件)。 這樣, 部分顧客就無需反覆前往商店了, 但也可能會惹惱對新產品沒有興趣的其他顧客。

我們似乎遇到了一個矛盾: 要麼讓顧客浪費時間檢查產品是否到貨, 要麼讓商店浪費資源去通知沒有需求的顧客。

解決方案

擁有一些值得關注的狀態的物件通常被稱為目標, 由於它要將自身的狀態改變通知給其他物件, 我們也將其稱為釋出者 (publisher)。 所有希望關注釋出者狀態變化的其他物件被稱為訂閱者 (subscribers)。

觀察者模式建議你為釋出者類新增訂閱機制, 讓每個物件都能訂閱或取消訂閱釋出者事件流。 不要害怕! 這並不像聽上去那麼複雜。 實際上, 該機制包括 1) 一個用於儲存訂閱者物件引用的列表成員變數; 2) 幾個用於新增或刪除該列表中訂閱者的公有方法。
訂閱機制

訂閱機制允許物件訂閱事件通知。

現在, 無論何時發生了重要的釋出者事件, 它都要遍歷訂閱者並呼叫其物件的特定通知方法。

實際應用中可能會有十幾個不同的訂閱者類跟蹤著同一個釋出者類的事件, 你不會希望釋出者與所有這些類相耦合的。 此外如果他人會使用釋出者類, 那麼你甚至可能會對其中的一些類一無所知。

因此, 所有訂閱者都必須實現同樣的介面, 釋出者僅透過該介面與訂閱者互動。 介面中必須宣告通知方法及其引數, 這樣釋出者在發出通知時還能傳遞一些上下文資料。

通知方法

釋出者呼叫訂閱者物件中的特定通知方法來通知訂閱者。

如果你的應用中有多個不同型別的釋出者, 且希望訂閱者可相容所有釋出者, 那麼你甚至可以進一步讓所有訂閱者遵循同樣的介面。 該介面僅需描述幾個訂閱方法即可。 這樣訂閱者就能在不與具體釋出者類耦合的情況下透過介面觀察釋出者的狀態。

真實世界類比

雜誌和報紙訂閱

雜誌和報紙訂閱。

如果你訂閱了一份雜誌或報紙, 那就不需要再去報攤查詢新出版的刊物了。 出版社 (即應用中的 “釋出者”) 會在刊物出版後 (甚至提前) 直接將最新一期寄送至你的郵箱中。

出版社負責維護訂閱者列表, 瞭解訂閱者對哪些刊物感興趣。 當訂閱者希望出版社停止寄送新一期的雜誌時, 他們可隨時從該列表中退出。

觀察者模式結構

觀察者設計模式的結構

  1. 釋出者 (Publisher) 會向其他物件傳送值得關注的事件。 事件會在釋出者自身狀態改變或執行特定行為後發生。 釋出者中包含一個允許新訂閱者加入和當前訂閱者離開列表的訂閱構架。

  2. 當新事件發生時, 傳送者會遍歷訂閱列表並呼叫每個訂閱者物件的通知方法。 該方法是在訂閱者介面中宣告的。

  3. 訂閱者 (Subscriber) 介面宣告瞭通知介面。 在絕大多數情況下, 該介面僅包含一個 update更新方法。 該方法可以擁有多個引數, 使釋出者能在更新時傳遞事件的詳細資訊。

  4. 具體訂閱者 (Concrete Subscribers) 可以執行一些操作來回應釋出者的通知。 所有具體訂閱者類都實現了同樣的介面, 因此釋出者不需要與具體類相耦合。

  5. 訂閱者通常需要一些上下文資訊來正確地處理更新。 因此, 釋出者通常會將一些上下文資料作為通知方法的引數進行傳遞。 釋出者也可將自身作為引數進行傳遞, 使訂閱者直接獲取所需的資料。

  6. 客戶端 (Client) 會分別建立釋出者和訂閱者物件, 然後為訂閱者註冊釋出者更新。

觀察者模式適合應用場景

當一個物件狀態的改變需要改變其他物件, 或實際物件是事先未知的或動態變化的時, 可使用觀察者模式。

當你使用圖形使用者介面類時通常會遇到一個問題。 比如, 你建立了自定義按鈕類並允許客戶端在按鈕中注入自定義程式碼, 這樣當使用者按下按鈕時就會觸發這些程式碼。

觀察者模式允許任何實現了訂閱者介面的物件訂閱釋出者物件的事件通知。 你可在按鈕中新增訂閱機制, 允許客戶端透過自定義訂閱類注入自定義程式碼。

當應用中的一些物件必須觀察其他物件時, 可使用該模式。 但僅能在有限時間內或特定情況下使用。

訂閱列表是動態的, 因此訂閱者可隨時加入或離開該列表。

實現方式

  1. 仔細檢查你的業務邏輯, 試著將其拆分為兩個部分: 獨立於其他程式碼的核心功能將作為釋出者; 其他程式碼則將轉化為一組訂閱類。

  2. 宣告訂閱者介面。 該介面至少應宣告一個 update方法。

  3. 宣告發布者介面並定義一些介面來在列表中新增和刪除訂閱物件。 記住釋出者必須僅透過訂閱者介面與它們進行互動。

  4. 確定存放實際訂閱列表的位置並實現訂閱方法。 通常所有型別的釋出者程式碼看上去都一樣, 因此將列表放置在直接擴充套件自發布者介面的抽象類中是顯而易見的。 具體釋出者會擴充套件該類從而繼承所有的訂閱行為。

    但是, 如果你需要在現有的類層次結構中應用該模式, 則可以考慮使用組合的方式: 將訂閱邏輯放入一個獨立的物件, 然後讓所有實際訂閱者使用該物件。

  5. 建立具體釋出者類。 每次釋出者發生了重要事件時都必須通知所有的訂閱者。

  6. 在具體訂閱者類中實現通知更新的方法。 絕大部分訂閱者需要一些與事件相關的上下文資料。 這些資料可作為通知方法的引數來傳遞。

    但還有另一種選擇。 訂閱者接收到通知後直接從通知中獲取所有資料。 在這種情況下, 釋出者必須透過更新方法將自身傳遞出去。 另一種不太靈活的方式是透過建構函式將釋出者與訂閱者永久性地連線起來。

  7. 客戶端必須生成所需的全部訂閱者, 並在相應的釋出者處完成註冊工作。

觀察者模式優缺點

  • (優點)開閉原則。 你無需修改釋出者程式碼就能引入新的訂閱者類 (如果是釋出者介面則可輕鬆引入釋出者類)。

  • (優點)你可以在執行時建立物件之間的聯絡。

  • (缺點)訂閱者的通知順序是隨機的。

結尾

原文連結:refactoringguru.cn/design-patterns...

本篇文章已標註原文連結,這篇文章講的通俗易懂,所以將原文大部分內容進行了搬運,留此記錄。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章