如何使用 Java8 實現觀察者模式?(上)

【編者按】本文做者是 BAE 系統公司的軟件工程師 Justin Albano。在本篇文章中,做者經過在 Java8 環境下實現觀察者模式的實例,進一步介紹了什麼是觀察者模式、專業化及其命名規則,供你們參考學習。本文系國內 ITOM 管理平臺 OneAPM 工程師編譯整理。html

觀察者(Observer)模式又名發佈-訂閱(Publish/Subscribe)模式,是四人組(GoF,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides)在1994合著的《設計模式:可複用面向對象軟件的基礎》中提出的(詳見書中293-313頁)。儘管這種模式已經有至關長的歷史,它仍然普遍適用於各類場景,甚至成爲了標準Java庫的一個組成部分。目前雖然已經有大量關於觀察者模式的文章,但它們都專一於在 Java 中的實現,卻忽視了開發者在Java中使用觀察者模式時遇到的各類問題。java

本文的寫做初衷就是爲了填補這一空白:本文主要介紹經過使用 Java8 架構實現觀察者模式,並在此基礎上進一步探討關於經典模式的複雜問題,包括匿名內部類、lambda 表達式、線程安全以及非平凡耗時長的觀察者實現。本文內容雖然並不全面,不少這種模式所涉及的複雜問題,遠不是一篇文章就能說清的。可是讀完本文,讀者能瞭解什麼是觀察者模式,它在Java中的通用性以及如何處理在 Java 中實現觀察者模式時的一些常見問題。算法

觀察者模式

根據 GoF 提出的經典定義,觀察者模式的主旨是:設計模式

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

什麼意思呢?不少軟件應用中,對象之間的狀態都是互相依賴的。例如,若是一個應用專一於數值數據加工,這個數據也許會經過圖形用戶界面(GUI)的表格或圖表來展示或者二者同時使用,也就是說,當底層數據更新時,相應的 GUI 組件也要更新。問題的關鍵在於如何作到底層數據更新時 GUI 組件也隨之更新,同時儘可能減少 GUI 組件和底層數據的耦合度。服務器

一種簡單且不可擴展的解決方案是給管理這些底層數據的對象該表格和圖像 GUI 組件的引用,使得對象能夠在底層數據變化時可以通知 GUI 組件。顯然,對於處理有更多 GUI 組件的複雜應用,這個簡單的解決方案很快顯示出其不足。例如,有20個 GUI 組件都依賴於底層數據,那麼管理底層數據的對象就須要維護指向這20個組件的引用。隨着依賴於相關數據的對象數量的增長,數據管理和對象之間的耦合度也變得難以控制。數據結構

另外一個更好的解決方案是容許對象註冊獲取感興趣數據更新的權限,當數據變化時,數據管理器就會通知這些對象。通俗地說就是,讓感興趣的數據對象告訴管理器:「當數據變化時請通知我」。此外,這些對象不只能夠註冊獲取更新通知,也能夠取消註冊,保證數據管理器在數據變化時再也不通知該對象。在 GoF 的原始定義中,註冊獲取更新的對象叫做「觀察者」(observer),對應的數據管理器叫做「目標」(Subject),觀察者感興趣的數據叫做「目標狀態」,註冊過程叫「添加」(attach),撤銷觀察的過程叫「移除」(detach)。前文已經提到觀察者模式又叫發佈-訂閱模式,能夠理解爲客戶訂閱關於目標的觀察者,當目標狀態更新時,目標把這些更新發布給訂閱者(這種設計模式擴展爲通用架構,稱爲發佈——訂閱架構)。這些概念能夠用下面的類圖表示:多線程

具體觀察者(ConcereteObserver)用來接收更新的狀態變化,同時將指向具體主題(ConcereteSubject)的引用傳遞給它的構造函數。這爲具體觀察者提供了指向具體主題的引用,在狀態變化時可由此得到更新。簡單來講,具體觀察者會被告知主題更新,同時用其構造函數中的引用來獲取具體主題的狀態,最後將這些檢索狀態對象存儲在具體觀察者的觀察狀態(observerState)屬性下。這一過程以下面的序列圖所示:架構

經典模式的專業化

儘管觀察者模式是通用的,但也有不少專業化的模式,最多見是如下兩種:框架

  1. 爲State對象提供一個參數,傳給觀察者調用的Update方法。在經典模式下,當觀察者被通知Subject狀態發生變化後,會直接從Subject得到其更新後狀態。這要求觀察者保存指向獲取狀態的對象引用。這樣就造成了一個循環引用,ConcreteSubject的引用指向其觀察者列表,ConcreteObserver的引用指向能得到主題狀態的ConcreteSubject。除了得到更新的狀態,觀察者和其註冊監聽的Subject間並無聯繫,觀察者關心的是State對象,而非Subject自己。也就是說,不少狀況下都將ConcreteObserver和ConcreteSubject強行聯繫一塊兒,相反,當ConcreteSubject調用Update函數時,將State對象傳遞給ConcreteObserver,兩者就無需關聯。ConcreteObserver和State對象之間關聯減少了觀察者和State之間的依賴程度(關聯和依賴的更多區別請參見Martin Fowler's的文章)。

  2. 將Subject抽象類和ConcreteSubject合併到一個 singleSubject類中。多數狀況下,Subject使用抽象類並不會提高程序的靈活性和可擴展性,所以,將這一抽象類和具體類合併簡化了設計。

這兩個專業化的模式組合後,其簡化類圖以下:

在這些專業化的模式中,靜態類結構大大簡化,類之間的相互做用也得以簡化。此時的序列圖以下:

專業化模式另外一特色是刪除了 ConcreteObserver 的成員變量 observerState。有時候具體觀察者並不須要保存Subject的最新狀態,而只須要監測狀態更新時 Subject 的狀態。例如,若是觀察者將成員變量的值更新到標準輸出上,就能夠刪除 observerState,這樣一來就刪除了ConcreteObserver和State類之間的關聯。

更常見的命名規則

經典模式甚至是前文提到的專業化模式都用的是attach,detach和observer等術語,而Java實現中不少都是用的不一樣的詞典,包括register,unregister,listener等。值得一提的是State是listener須要監測變化的全部對象的統稱,狀態對象的具體名稱須要看觀察者模式用到的場景。例如,在listener監聽事件發生場景下的觀察者模式,已註冊的listener將會在事件發生時收到通知,此時的狀態對象就是event,也就是事件是否發生。

平時實際應用中目標的命名不多包含Subject。例如,建立一個關於動物園的應用,註冊多個監聽器用於觀察Zoo類,並在新動物進入動物園時收到通知。該案例中的目標是Zoo類,爲了和所給問題域保持術語一致,將不會用到Subject這樣的詞彙,也就是說Zoo類不會命名爲ZooSubject。

監聽器的命名通常都會跟着Listener後綴,例如前文提到的監測新動物加入的監聽器會命名爲AnimalAddedListener。相似的,register,、unregister和notify等函數命名常會以其對應的監聽器名做後綴,例如AnimalAddedListener的register、unregister、notify函數會被命名爲registerAnimalAddedListener、 unregisterAnimalAddedListener和notifyAnimalAddedListeners,須要注意的是notify函數名的s,由於notify函數處理的是多個而非單一監聽器。

這種命名方式會顯得冗長,並且一般一個subject會註冊多個類型的監聽器,如前面提到的動物園的例子,Zoo內除了註冊監聽動物新增的監聽器,還需註冊監聽動物減小監聽器,此時就會有兩種register函數:(registerAnimalAddedListener和 registerAnimalRemovedListener,這種方式處理,監聽器的類型做爲一個限定符,表示其應觀察者的類型。另外一解決方案是建立一個registerListener函數而後重載,可是方案一能更方便的知道哪一個監聽器正在監聽,重載是比較小衆的作法。

另外一慣用語法是用on前綴而不是update,例如update函數命名爲onAnimalAdded而不是updateAnimalAdded。這種狀況在監聽器得到一個序列的通知時更常見,如向list中新增一個動物,但不多用於更新一個單獨的數據,好比動物的名字。

接下來本文將使用Java的符號規則,雖然符號規則不會改變系統的真實設計和實現,可是使用其餘開發者都熟悉的術語是很重要的開發準則,所以要熟悉上文描述的Java中的觀察者模式符號規則。下文將在Java8環境下用一個簡單例子來闡述上述概念。

一個簡單的實例

仍是前面提到的動物園的例子,使用Java8的API接口實現一個簡單的系統,說明觀察者模式的基本原理。問題描述爲:

建立一個系統zoo,容許用戶監聽和撤銷監聽添加新對象animal的狀態,另外再建立一個具體監聽器,負責輸出新增動物的name。

根據前面對觀察者模式的學習知道實現這樣的應用須要建立4個類,具體是:

  1. Zoo類:即模式中的主題,負責存儲動物園中的全部動物,並在新動物加入時通知全部已註冊的監聽器。

  2. Animal類:表明動物對象。

  3. AnimalAddedListener類:即觀察者接口。

  4. PrintNameAnimalAddedListener:具體的觀察者類,負責輸出新增動物的name。

首先咱們建立一個Animal類,它是一個包含name成員變量、構造函數、getter和setter方法的簡單Java對象,代碼以下:

public class Animal {
    private String name;
    public Animal (String name) {
        this.name = name;
    }
    public String getName () {
        return this.name;
    }
    public void setName (String name) {
        this.name = name;
    }
}

用這個類表明動物對象,接下來就能夠建立AnimalAddedListener接口了:

public interface AnimalAddedListener {
    public void onAnimalAdded (Animal animal);
}

前面兩個類很簡單,就再也不詳細介紹,接下來建立Zoo類:

public class Zoo {
    private List<Animal> animals = new ArrayList<>();
    private List<AnimalAddedListener> listeners = new ArrayList<>();
    public void addAnimal (Animal animal) {
        // Add the animal to the list of animals
        this.animals.add(animal);
        // Notify the list of registered listeners
        this.notifyAnimalAddedListeners(animal);
    }
    public void registerAnimalAddedListener (AnimalAddedListener listener) {
        // Add the listener to the list of registered listeners
        this.listeners.add(listener);
    }
    public void unregisterAnimalAddedListener (AnimalAddedListener listener) {
        // Remove the listener from the list of the registered listeners
        this.listeners.remove(listener);
    }
    protected void notifyAnimalAddedListeners (Animal animal) {
        // Notify each of the listeners in the list of registered listeners
        this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
    }
}

這個類比前面兩個都複雜,其包含兩個list,一個用來存儲動物園中全部動物,另外一個用來存儲全部的監聽器,鑑於animals和listener集合存儲的對象都很簡單,本文選擇了ArrayList來存儲。存儲監聽器的具體數據結構要視問題而定,好比對於這裏的動物園問題,若是監聽器有優先級,那就應該選擇其餘的數據結構,或者重寫監聽器的register算法。

註冊和移除的實現都是簡單的委託方式:各個監聽器做爲參數從監聽者的監聽列表增長或者移除。notify函數的實現與觀察者模式的標準格式稍微偏離,它包括輸入參數:新增長的animal,這樣一來notify函數就能夠把新增長的animal引用傳遞給監聽器了。用streams API的forEach函數遍歷監聽器,對每一個監聽器執行theonAnimalAdded函數。

在addAnimal函數中,新增的animal對象和監聽器各自添加到對應list。若是不考慮通知過程的複雜性,這一邏輯應包含在方便調用的方法中,只須要傳入指向新增animal對象的引用便可,這就是通知監聽器的邏輯實現封裝在notifyAnimalAddedListeners函數中的緣由,這一點在addAnimal的實現中也提到過。

除了notify函數的邏輯問題,須要強調一下對notify函數可見性的爭議問題。在經典的觀察者模型中,如GoF在設計模式一書中第301頁所說,notify函數是public型的,然而儘管在經典模式中用到,這並不意味着必須是public的。選擇可見性應該基於應用,例如本文的動物園的例子,notify函數是protected類型,並不要求每一個對象均可以發起一個註冊觀察者的通知,只需保證對象能從父類繼承該功能便可。固然,也並不是徹底如此,須要弄清楚哪些類能夠激活notify函數,而後再由此肯定函數的可見性。

接下來須要實現PrintNameAnimalAddedListener類,這個類用System.out.println方法將新增動物的name輸出,具體代碼以下:

public class PrintNameAnimalAddedListener implements AnimalAddedListener {
    @Override
    public void updateAnimalAdded (Animal animal) {
        // Print the name of the newly added animal
        System.out.println("Added a new animal with name '" + animal.getName() + "'");
    }
}

最後要實現驅動應用的主函數:

public class Main {
    public static void main (String[] args) {
        // Create the zoo to store animals
        Zoo zoo = new Zoo();
        // Register a listener to be notified when an animal is added
        zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener());
        // Add an animal notify the registered listeners
        zoo.addAnimal(new Animal("Tiger"));
    }
}

主函數只是簡單的建立了一個zoo對象,註冊了一個輸出動物name的監聽器,並新建了一個animal對象以觸發已註冊的監聽器,最後的輸出爲:

Added a new animal with name 'Tiger'

新增監聽器

當監聽器從新創建並將其添加到Subject時,觀察者模式的優點就充分顯示出來。例如,想添加一個計算動物園中動物總數的監聽器,只須要新建一個具體的監聽器類並註冊到Zoo類便可,而無需對zoo類作任何修改。添加計數監聽器CountingAnimalAddedListener代碼以下:

public class CountingAnimalAddedListener implements AnimalAddedListener {
    private static int animalsAddedCount = 0;
    @Override
    public void updateAnimalAdded (Animal animal) {
        // Increment the number of animals
        animalsAddedCount++;
        // Print the number of animals
        System.out.println("Total animals added: " + animalsAddedCount);
    }
}

修改後的main函數以下:

public class Main {
    public static void main (String[] args) {
        // Create the zoo to store animals
        Zoo zoo = new Zoo();
        // Register listeners to be notified when an animal is added
        zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener());
        zoo.registerAnimalAddedListener(new CountingAnimalAddedListener());
        // Add an animal notify the registered listeners
        zoo.addAnimal(new Animal("Tiger"));
        zoo.addAnimal(new Animal("Lion"));
        zoo.addAnimal(new Animal("Bear"));
    }
}

輸出結果爲:

Added a new animal with name 'Tiger'
Total animals added: 1
Added a new animal with name 'Lion'
Total animals added: 2
Added a new animal with name 'Bear'
Total animals added: 3

使用者可在僅修改監聽器註冊代碼的狀況下,建立任意監聽器。具備此可擴展性主要是由於Subject和觀察者接口關聯,而不是直接和ConcreteObserver關聯。只要接口不被修改,調用接口的Subject就無需修改。

匿名內部類,Lambda函數和監聽器註冊

Java8的一大改進是增長了功能特性,如增長了lambda函數。在引進lambda函數以前,Java經過匿名內部類提供了相似的功能,這些類在不少已有的應用中仍在使用。在觀察者模式下,隨時能夠建立新的監聽器而無需建立具體觀察者類,例如,PrintNameAnimalAddedListener類能夠在main函數中用匿名內部類實現,具體實現代碼以下:

public class Main {
    public static void main (String[] args) {
        // Create the zoo to store animals
        Zoo zoo = new Zoo();
        // Register listeners to be notified when an animal is added
        zoo.registerAnimalAddedListener(new AnimalAddedListener() {
            @Override
            public void updateAnimalAdded (Animal animal) {
                // Print the name of the newly added animal
                System.out.println("Added a new animal with name '" + animal.getName() + "'");
            }
        });
        // Add an animal notify the registered listeners
        zoo.addAnimal(new Animal("Tiger"));
    }
}

相似的,lambda函數也能夠用以完成此類任務:

public class Main {
    public static void main (String[] args) {
        // Create the zoo to store animals
        Zoo zoo = new Zoo();
        // Register listeners to be notified when an animal is added
        zoo.registerAnimalAddedListener(
            (animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'")
        );
        // Add an animal notify the registered listeners
        zoo.addAnimal(new Animal("Tiger"));
    }
}

須要注意的是lambda函數僅適用於監聽器接口只有一個函數的狀況,這個要求雖然看起來嚴格,但實際上不少監聽器都是單一函數的,如示例中的AnimalAddedListener。若是接口有多個函數,能夠選擇使用匿名內部類。

隱式註冊建立的監聽器存在此類問題:因爲對象是在註冊調用的範圍內建立的,因此不可能將引用存儲一個到具體監聽器。這意味着,經過lambda函數或者匿名內部類註冊的監聽器不能夠撤銷註冊,由於撤銷函數須要傳入已經註冊監聽器的引用。解決這個問題的一個簡單方法是在registerAnimalAddedListener函數中返回註冊監聽器的引用。如此一來,就能夠撤銷註冊用lambda函數或匿名內部類建立的監聽器,改進後的方法代碼以下:

public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {
    // Add the listener to the list of registered listeners
    this.listeners.add(listener); 
    return listener;
}

從新設計的函數交互的客戶端代碼以下:

public class Main {
    public static void main (String[] args) {
        // Create the zoo to store animals
        Zoo zoo = new Zoo();
        // Register listeners to be notified when an animal is added
        AnimalAddedListener listener = zoo.registerAnimalAddedListener(
            (animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'")
        );
        // Add an animal notify the registered listeners
        zoo.addAnimal(new Animal("Tiger"));
        // Unregister the listener
        zoo.unregisterAnimalAddedListener(listener);
        // Add another animal, which will not print the name, since the listener
        // has been previously unregistered
        zoo.addAnimal(new Animal("Lion"));
    }
}

此時的結果輸出只有Added a new animal with name 'Tiger',由於在第二個animal加入以前監聽器已經撤銷了:

Added a new animal with name 'Tiger'

若是採用更復雜的解決方案,register函數也能夠返回receipt類,以便unregister監聽器調用,例如:

public class AnimalAddedListenerReceipt {
    private final AnimalAddedListener listener;
    public AnimalAddedListenerReceipt (AnimalAddedListener listener) {
        this.listener = listener;
    }
    public final AnimalAddedListener getListener () {
        return this.listener;
    }
}

receipt會做爲註冊函數的返回值,以及撤銷註冊函數輸入參數,此時的zoo實現以下所示:

public class ZooUsingReceipt {
    // ...Existing attributes and constructor...
    public AnimalAddedListenerReceipt registerAnimalAddedListener (AnimalAddedListener listener) {
        // Add the listener to the list of registered listeners
        this.listeners.add(listener);
        return new AnimalAddedListenerReceipt(listener);
    }
    public void unregisterAnimalAddedListener (AnimalAddedListenerReceipt receipt) {
        // Remove the listener from the list of the registered listeners
        this.listeners.remove(receipt.getListener());
    }
    // ...Existing notification method...
}

上面描述的接收實現機制容許保存信息供監聽器撤銷時調用的,也就是說若是撤銷註冊算法依賴於Subject註冊監聽器時的狀態,則此狀態將被保存,若是撤銷註冊只須要指向以前註冊監聽器的引用,這樣的話接收技術則顯得麻煩,不推薦使用。

除了特別複雜的具體監聽器,最多見的註冊監聽器的方法是經過lambda函數或經過匿名內部類註冊。固然,也有例外,那就是包含subject實現觀察者接口的類和註冊一個包含調用該引用目標的監聽器。以下面代碼所示的案例:

public class ZooContainer implements AnimalAddedListener {
    private Zoo zoo = new Zoo();
    public ZooContainer () {
        // Register this object as a listener
        this.zoo.registerAnimalAddedListener(this);
    }
    public Zoo getZoo () {
        return this.zoo;
    }
    @Override
        public void updateAnimalAdded (Animal animal) {
        System.out.println("Added animal with name '" + animal.getName() + "'");
    }
    public static void main (String[] args) {
        // Create the zoo container
        ZooContainer zooContainer = new ZooContainer();
        // Add an animal notify the innerally notified listener
        zooContainer.getZoo().addAnimal(new Animal("Tiger"));
    }
}

這種方法只適用於簡單狀況並且代碼看起來不夠專業,儘管如此,它仍是深受現代Java開發人員的喜好,所以瞭解這個例子的工做原理頗有必要。由於ZooContainer實現了AnimalAddedListener接口,那麼ZooContainer的實例(或者說對象)就能夠註冊爲AnimalAddedListener。ZooContainer類中,該引用表明當前對象即ZooContainer的一個實例,因此能夠被用做AnimalAddedListener。

一般,不是要求全部的container類都實現此類功能,並且實現監聽器接口的container類只能調用Subject的註冊函數,只是簡單把該引用做爲監聽器的對象傳給register函數。在接下來的章節中,將介紹多線程環境的常見問題和解決方案。

(編譯自:https://dzone.com/articles/the-observer-pattern-using-modern-java)

OneAPM 爲您提供端到端的 Java 應用性能解決方案,咱們支持全部常見的 Java 框架及應用服務器,助您快速發現系統瓶頸,定位異常根本緣由。分鐘級部署,即刻體驗,Java 監控歷來沒有如此簡單。想閱讀更多技術文章,請訪問 OneAPM 官方技術博客

本文轉自 OneAPM 官方博客

相關文章
相關標籤/搜索