觀察者模式(學習筆記)

  1. 意圖

  定義對象間的一種一對多的依賴關係,當一個對象的狀態發生變化時,全部依賴於它的對象都獲得通知並被自動更新java

  2. 動機    

  假設這樣一種狀況,顧客對某個特定品牌的產品很是感興趣(例如最新型號的 iPhone 手機),而該產品很快將會在商店裏出售。顧客能夠天天來商店看看產品是否到貨。但若是商品還沒有到貨時,絕大多數來到商店的顧客都會空手而歸。另外一方面,每次新產品到貨時,商店能夠向全部顧客發送郵件(可能會被視爲垃圾郵件)。這樣,部分顧客就無需反覆前往商店了,但也可能會惹惱對新產品沒有興趣的其餘顧客。編輯器

  咱們彷佛遇到了一個矛盾:要麼讓顧客浪費時間檢查產品是否到貨,要麼讓商店浪費資源去通知沒有需求的顧客。觀察者模式能夠解決這一問題。  ide

  觀察者模式爲發佈者(將自身的狀態改變通知給其餘對象)類添加訂閱機制,讓每一個對象都能訂閱或取消訂閱發佈者事件流。該機制包括:this

  1)一個用於存儲訂閱者(全部但願關注發佈者狀態變化的其餘對象)對象引用的列表成員變量;spa

  2)幾個用於添加或刪除該列表中訂閱者的公有方法。日誌

        

  這樣,不管什麼時候發生了重要的發佈者事件,它都要遍歷訂閱者並調用其對象的特定通知方法。在實際應用中可能會有十幾個不一樣的訂閱者類跟蹤着同一個發佈者類的事件, 咱們不但願發佈者與全部這些類相耦合的。所以,全部訂閱者都必須實現一樣的接口,發佈者僅經過該接口與訂閱者交互。接口中必須聲明通知方法及其參數,這樣發佈者在發出通知時還能傳遞一些上下文數據。若是在應用中存在不一樣類型的發佈者,且但願一個訂閱者能夠同時訂閱多個發佈者。須要讓全部訂閱者遵循相同的接口,並在該接口中描述幾個訂閱方法(須要將發佈者做爲參數傳入方法中)便可。這樣訂閱者就能在不與具體發佈者類耦合的狀況下經過接口觀察發佈者的狀態code

          

  3. 適用性

  • 一個抽象模型有兩個方面,其中一個方面依賴於另外一方面。將這二者封裝在獨立的對象中,以使它們能夠各自獨立的改變和複用
  • 對一個對象地改變須要同時改變其它對象,而不知道具體有多少對象有待改變
  • 一個對象必須通知其餘對象,而它又不能假定其餘對象是誰。換言之,你不但願這些對象是緊密耦合的

  4. 結構

         

  5. 效果

  Observer模式容許你獨立地改變目標和觀察者orm

  1. 目標和觀察者間地抽象耦合   一個目標所知道的僅僅是它有一系列觀察者,每一個都符合抽象的Observer類的簡單接口。目標不知道任何一個觀察者屬於哪一個具體的類。這樣目標和觀察者之間地耦合是抽象和最小的。server

  2. 支持廣播通訊    不像一般的請求,目標發送的通知不須要指定它的接收者。通知被自動廣播給全部已向該目標對象登記的對象。另外,處理仍是忽略一個通知取決於觀察者對象

  3. 意外的更新      因爲一個觀察者並不知道其餘觀察者地存在,它可能對改變目標的最終代價一無所知

  6. 代碼實現    

  本例中,觀察者模式在文本編輯器的對象之間創建了間接的合做關係。每當編輯器 (Editor)對象改變時,它都會通知其訂閱者。 ​郵件通知監聽器 (Email­Notification­Listener)和日誌開啓監聽器 (Log­Open­Listener)都將經過執行其基本行爲來對這些通知作出反應。
訂閱者類不與編輯器類相耦合,且能在須要時在其餘應用中複用。 ​編輯器類僅依賴於抽象訂閱者接口。這樣就能容許在不改變編輯器代碼的狀況下添加新的訂閱者類型。

  publisher/EventManager.java: 基礎發佈者

package observer.publisher;

import observer.listeners.EventListener;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:01
 */
public class EventManager {
    Map<String, List<EventListener>> listeners = new HashMap<>();

    public EventManager(String... operations) {
        for (String operation : operations) {
            this.listeners.put(operation, new ArrayList<>());
        }
    }

    public void subscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.add(listener);
    }

    public void unsubscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.remove(listener);
    }

    public void notify(String eventType, File file) {
        List<EventListener> users = listeners.get(eventType);
        for (EventListener listener : users) {
            listener.update(eventType, file);
        }
    }
}

  editor/Editor.java: 具體發佈者,由其餘對象追蹤

package observer.editor;

import observer.publisher.EventManager;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:01
 */
public class Editor {
    public EventManager events;
    private File file;

    public Editor() {
        this.events = new EventManager("open", "save");
    }

    public void openFile(String filePath) {
        this.file = new File(filePath);
        events.notify("open", file);
    }

    public void saveFile() throws Exception {
        if (this.file != null) {
            events.notify("save", file);
        } else {
            throw new Exception("Please open a file first.");
        }
    }
}

  listeners/EventListener.java: 通用觀察者接口

package observer.listeners;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:02
 */
public interface EventListener {
    void update(String eventType, File file);
}

  listeners/EmailNotificationListener.java: 收到通知後發送郵件

package observer.listeners;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:02
 */
public class EmailNotificationListener implements EventListener{
    private String email;

    public EmailNotificationListener(String email) {
        this.email = email;
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}

  listeners/LogOpenListener.java: 收到通知後在日誌中記錄一條消息

package observer.listeners;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:03
 */
public class LogOpenListener implements EventListener{
    private File log;

    public LogOpenListener(String fileName) {
        this.log = new File(fileName);
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }

}

  Demo.java: 客戶端代碼

package observer;

import observer.editor.Editor;
import observer.listeners.EmailNotificationListener;
import observer.listeners.LogOpenListener;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:00
 */
public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.events.subscribe("open", new LogOpenListener("/path/to/log/file.txt"));
        editor.events.subscribe("save", new EmailNotificationListener("admin@example.com"));

        try {
            editor.openFile("test.txt");
            editor.saveFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  運行結果

Save to log \path\to\log\file.txt: Someone has performed open operation with the following file: test.txt
Email to admin@example.com: Someone has performed save operation with the following file: test.txt

    

  7. 實現

  1)建立目標到其觀察者之間的映射     一個目標對象跟蹤它應通知的觀察者的最簡單的方法是顯式地在目標中保存對它們地引用。然而,當目標不少而觀察者較少時,這樣存儲可能代價過高。一個解決辦法是用時間換空間,用一個關聯查找機制(例如一個hash表)來維護目標到觀察者地映射。這樣一個沒有觀察者的目標就不產生存儲開銷。但另外一方面,這一方法增長了訪問觀察者的開銷

  2)觀察多個目標    有時候,一個觀察者依賴於多個目標。例如,一個表格對象可能依賴於多個數據源。在這種狀況下,必須擴展update接口,目標對象能夠簡單的將本身做爲Update操做地一個參數,讓觀察者知道應該去檢查哪一個目標

  3)誰觸發更新     目標和它的觀察者依賴於通知機制來保持一致。但到底哪一個對象調用Notify來觸發更新? 這裏有兩個選擇:

  • 由目標對象的狀態設定操做在改變目標對象的狀態後自動調用Notify。這種方法的優勢是客戶不須要記住要在目標對象上調用Notify,缺點是多個連續的操做會產生屢次連續的更新,可能效率較低
  • 讓客戶負責在合適的時候調用notify。這樣作的優勢是客戶在一系列狀態改變完成後一次性的觸發更新,避免了沒必要要的中間更新。缺點是給客戶增長了觸發更新的責任。因爲客戶可能會忘記調用Notify,這種方式交易出錯

  4)在發出通知前確保目標的狀態自身是一致的       在發出通知前確保狀態自身一致這一點很重要,由於觀察者在更新其狀態的過程當中須要查詢目標的當前狀態。可使用模板方法發送通知來避免這種錯誤。定義那些子類能夠重定義的原語操做,並將Notify做爲模板方法中的最後一個操做,這樣當子類重定義Subject的操做時,還能夠保證該對象的狀態是自身一致的。另外,最好在文檔中註明哪一個Subject操做觸發通知

  5)避免特定於觀察者的更新協議——推/拉模型     觀察者模式的實現常常須要讓目標廣播關於其改變的其餘一些信息。目標將這些信息做爲Update操做的一個參數傳遞出去。一個極端狀況是,目標向觀察者發送關於改變的詳細信息,而無論它們須要與否,即推模型。另外一個極端是拉模型,目標除最小通知外什麼也不送出,而在此以後由觀察者顯式的向目標詢問細節。拉模型強調的是目標不知道它的觀察者,而推模型假定目標知道一些觀察者須要的信息。推模型使得觀察者相對難以複用。另外一方面,拉模型效率會較差,由於觀察者對象須要在沒有目標對象的幫助下,肯定什麼改變了

  6)顯式地指定感興趣的改變       能夠經過擴展目標的註冊接口,讓觀察者註冊爲僅對特定事件感興趣的觀察者。如上面例子中,將EventType做爲參數傳遞給Notify 和Update方法

  7)封裝複雜的更新語義        當目標和觀察者間的依賴關係特別複雜時,可能須要一個維護這些關係的對象,即ChangeManager(更改管理器)。其目的是儘可能減小觀察者反映其目標狀態變化所需的工做量。例如,若是一個操做涉及幾個相互依賴的目標進行改動,就必須保證在全部的目標更新完畢後,才一次性的通知它們的觀察者,而不是每一個目標都通知觀察者。ChangeManager是一個Mediator模式的實例。相比於,SimpleChangeManager,當一個觀察者觀察多個目標時,DAGChangeManager保證觀察者僅接收一個更新

                   

  8. 與其餘模式的關係

  • 責任鏈模式、命令模式、中介者模式和觀察者模式用於處理請求發送者和接收者之間的不一樣鏈接方式:
    責任鏈按照順序將請求動態傳遞給一系列的潛在接收者,直至其中一名接收者對請求進行處理
    命令在發送者和請求者之間創建單向鏈接
    中介者清除了發送者和請求者之間的直接鏈接,強制它們經過一箇中介對象進行間接溝通
    觀察者容許接收者動態地訂閱或取消接收請求

  • 中介者和觀察者有的時候會很是類似
    中介者的主要目標是消除一系列系統組件之間的相互依賴。這些組件將依賴於同一個中介者對象。觀察者的目標是在對象之間創建動態的單向鏈接,使得部分對象可做爲其餘對象的附屬發揮做用
    有一種流行的中介者模式實現方式依賴於觀察者。中介者對象擔當發佈者的角色,其餘組件則做爲訂閱者,能夠訂閱中介者的事件或取消訂閱。當中介者以這種方式實現時,它可能看上去與觀察者很是類似

  9. 已知應用  

  觀察者模式在 Java 代碼中很常見,特別是在 GUI 組件中。它提供了在不與其餘對象所屬類耦合的狀況下對其事件作出反應的方式  這裏是核心 Java 程序庫中該模式的一些示例:  java.util.Observer/ java.util.Observable (極少在真實世界中使用)  java.util.EventListener的全部實現 (幾乎普遍存在於 Swing 組件中)  javax.servlet.http.HttpSessionBindingListener  javax.servlet.http.HttpSessionAttributeListener  javax.faces.event.PhaseListener  識別方法: 該模式能夠經過將對象存儲在列表中的訂閱方法, 和對於面向該列表中對象的更新方法的調用來識別

相關文章
相關標籤/搜索