亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

全面解析Java8觀察者模式

 更新時(shí)間:2016年02月22日 16:08:54   作者:仲浩  
這篇文章主要為大家全面解析Java8觀察者模式,通過在 Java8 環(huán)境下實(shí)現(xiàn)觀察者模式的實(shí)例,進(jìn)一步介紹了什么是觀察者模式、專業(yè)化及其命名規(guī)則,感興趣的小伙伴們可以參考一下

觀察者(Observer)模式又名發(fā)布-訂閱(Publish/Subscribe)模式,是四人組(GoF,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides)在1994合著的《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》中提出的(詳見書中293-313頁)。盡管這種模式已經(jīng)有相當(dāng)長(zhǎng)的歷史,它仍然廣泛適用于各種場(chǎng)景,甚至成為了標(biāo)準(zhǔn)Java庫的一個(gè)組成部分。目前雖然已經(jīng)有大量關(guān)于觀察者模式的文章,但它們都專注于在 Java 中的實(shí)現(xiàn),卻忽視了開發(fā)者在Java中使用觀察者模式時(shí)遇到的各種問題。

本文的寫作初衷就是為了填補(bǔ)這一空白:本文主要介紹通過使用 Java8 架構(gòu)實(shí)現(xiàn)觀察者模式,并在此基礎(chǔ)上進(jìn)一步探討關(guān)于經(jīng)典模式的復(fù)雜問題,包括匿名內(nèi)部類、lambda 表達(dá)式、線程安全以及非平凡耗時(shí)長(zhǎng)的觀察者實(shí)現(xiàn)。本文內(nèi)容雖然并不全面,很多這種模式所涉及的復(fù)雜問題,遠(yuǎn)不是一篇文章就能說清的。但是讀完本文,讀者能了解什么是觀察者模式,它在Java中的通用性以及如何處理在 Java 中實(shí)現(xiàn)觀察者模式時(shí)的一些常見問題。

觀察者模式
根據(jù) GoF 提出的經(jīng)典定義,觀察者模式的主旨是:

定義對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新。

什么意思呢?很多軟件應(yīng)用中,對(duì)象之間的狀態(tài)都是互相依賴的。例如,如果一個(gè)應(yīng)用專注于數(shù)值數(shù)據(jù)加工,這個(gè)數(shù)據(jù)也許會(huì)通過圖形用戶界面(GUI)的表格或圖表來展現(xiàn)或者兩者同時(shí)使用,也就是說,當(dāng)?shù)讓訑?shù)據(jù)更新時(shí),相應(yīng)的 GUI 組件也要更新。問題的關(guān)鍵在于如何做到底層數(shù)據(jù)更新時(shí) GUI 組件也隨之更新,同時(shí)盡量減小 GUI 組件和底層數(shù)據(jù)的耦合度。

一種簡(jiǎn)單且不可擴(kuò)展的解決方案是給管理這些底層數(shù)據(jù)的對(duì)象該表格和圖像 GUI 組件的引用,使得對(duì)象可以在底層數(shù)據(jù)變化時(shí)能夠通知 GUI 組件。顯然,對(duì)于處理有更多 GUI 組件的復(fù)雜應(yīng)用,這個(gè)簡(jiǎn)單的解決方案很快顯示出其不足。例如,有20個(gè) GUI 組件都依賴于底層數(shù)據(jù),那么管理底層數(shù)據(jù)的對(duì)象就需要維護(hù)指向這20個(gè)組件的引用。隨著依賴于相關(guān)數(shù)據(jù)的對(duì)象數(shù)量的增加,數(shù)據(jù)管理和對(duì)象之間的耦合度也變得難以控制。

另一個(gè)更好的解決方案是允許對(duì)象注冊(cè)獲取感興趣數(shù)據(jù)更新的權(quán)限,當(dāng)數(shù)據(jù)變化時(shí),數(shù)據(jù)管理器就會(huì)通知這些對(duì)象。通俗地說就是,讓感興趣的數(shù)據(jù)對(duì)象告訴管理器:“當(dāng)數(shù)據(jù)變化時(shí)請(qǐng)通知我”。此外,這些對(duì)象不僅可以注冊(cè)獲取更新通知,也可以取消注冊(cè),保證數(shù)據(jù)管理器在數(shù)據(jù)變化時(shí)不再通知該對(duì)象。在 GoF 的原始定義中,注冊(cè)獲取更新的對(duì)象叫作“觀察者”(observer),對(duì)應(yīng)的數(shù)據(jù)管理器叫作“目標(biāo)”(Subject),觀察者感興趣的數(shù)據(jù)叫作“目標(biāo)狀態(tài)”,注冊(cè)過程叫“添加”(attach),撤銷觀察的過程叫“移除”(detach)。前文已經(jīng)提到觀察者模式又叫發(fā)布-訂閱模式,可以理解為客戶訂閱關(guān)于目標(biāo)的觀察者,當(dāng)目標(biāo)狀態(tài)更新時(shí),目標(biāo)把這些更新發(fā)布給訂閱者(這種設(shè)計(jì)模式擴(kuò)展為通用架構(gòu),稱為發(fā)布——訂閱架構(gòu))。這些概念可以用下面的類圖表示:

具體觀察者(ConcereteObserver)用來接收更新的狀態(tài)變化,同時(shí)將指向具體主題(ConcereteSubject)的引用傳遞給它的構(gòu)造函數(shù)。這為具體觀察者提供了指向具體主題的引用,在狀態(tài)變化時(shí)可由此獲得更新。簡(jiǎn)單來說,具體觀察者會(huì)被告知主題更新,同時(shí)用其構(gòu)造函數(shù)中的引用來獲取具體主題的狀態(tài),最后將這些檢索狀態(tài)對(duì)象存儲(chǔ)在具體觀察者的觀察狀態(tài)(observerState)屬性下。這一過程如下面的序列圖所示:

經(jīng)典模式的專業(yè)化
盡管觀察者模式是通用的,但也有很多專業(yè)化的模式,最常見是以下兩種:

1、為State對(duì)象提供一個(gè)參數(shù),傳給觀察者調(diào)用的Update方法。在經(jīng)典模式下,當(dāng)觀察者被通知Subject狀態(tài)發(fā)生變化后,會(huì)直接從Subject獲得其更新后狀態(tài)。這要求觀察者保存指向獲取狀態(tài)的對(duì)象引用。這樣就形成了一個(gè)循環(huán)引用,ConcreteSubject的引用指向其觀察者列表,ConcreteObserver的引用指向能獲得主題狀態(tài)的ConcreteSubject。除了獲得更新的狀態(tài),觀察者和其注冊(cè)監(jiān)聽的Subject間并沒有聯(lián)系,觀察者關(guān)心的是State對(duì)象,而非Subject本身。也就是說,很多情況下都將ConcreteObserver和ConcreteSubject強(qiáng)行聯(lián)系一起,相反,當(dāng)ConcreteSubject調(diào)用Update函數(shù)時(shí),將State對(duì)象傳遞給ConcreteObserver,二者就無需關(guān)聯(lián)。ConcreteObserver和State對(duì)象之間關(guān)聯(lián)減小了觀察者和State之間的依賴程度(關(guān)聯(lián)和依賴的更多區(qū)別請(qǐng)參見Martin Fowler's的文章)。

2、將Subject抽象類和ConcreteSubject合并到一個(gè) singleSubject類中。多數(shù)情況下,Subject使用抽象類并不會(huì)提升程序的靈活性和可擴(kuò)展性,因此,將這一抽象類和具體類合并簡(jiǎn)化了設(shè)計(jì)。

這兩個(gè)專業(yè)化的模式組合后,其簡(jiǎn)化類圖如下:

在這些專業(yè)化的模式中,靜態(tài)類結(jié)構(gòu)大大簡(jiǎn)化,類之間的相互作用也得以簡(jiǎn)化。此時(shí)的序列圖如下:

專業(yè)化模式另一特點(diǎn)是刪除了 ConcreteObserver 的成員變量 observerState。有時(shí)候具體觀察者并不需要保存Subject的最新狀態(tài),而只需要監(jiān)測(cè)狀態(tài)更新時(shí) Subject 的狀態(tài)。例如,如果觀察者將成員變量的值更新到標(biāo)準(zhǔn)輸出上,就可以刪除 observerState,這樣一來就刪除了ConcreteObserver和State類之間的關(guān)聯(lián)。

更常見的命名規(guī)則
經(jīng)典模式甚至是前文提到的專業(yè)化模式都用的是attach,detach和observer等術(shù)語,而Java實(shí)現(xiàn)中很多都是用的不同的詞典,包括register,unregister,listener等。值得一提的是State是listener需要監(jiān)測(cè)變化的所有對(duì)象的統(tǒng)稱,狀態(tài)對(duì)象的具體名稱需要看觀察者模式用到的場(chǎng)景。例如,在listener監(jiān)聽事件發(fā)生場(chǎng)景下的觀察者模式,已注冊(cè)的listener將會(huì)在事件發(fā)生時(shí)收到通知,此時(shí)的狀態(tài)對(duì)象就是event,也就是事件是否發(fā)生。

平時(shí)實(shí)際應(yīng)用中目標(biāo)的命名很少包含Subject。例如,創(chuàng)建一個(gè)關(guān)于動(dòng)物園的應(yīng)用,注冊(cè)多個(gè)監(jiān)聽器用于觀察Zoo類,并在新動(dòng)物進(jìn)入動(dòng)物園時(shí)收到通知。該案例中的目標(biāo)是Zoo類,為了和所給問題域保持術(shù)語一致,將不會(huì)用到Subject這樣的詞匯,也就是說Zoo類不會(huì)命名為ZooSubject。

監(jiān)聽器的命名一般都會(huì)跟著Listener后綴,例如前文提到的監(jiān)測(cè)新動(dòng)物加入的監(jiān)聽器會(huì)命名為AnimalAddedListener。類似的,register,、unregister和notify等函數(shù)命名常會(huì)以其對(duì)應(yīng)的監(jiān)聽器名作后綴,例如AnimalAddedListener的register、unregister、notify函數(shù)會(huì)被命名為registerAnimalAddedListener、 unregisterAnimalAddedListener和notifyAnimalAddedListeners,需要注意的是notify函數(shù)名的s,因?yàn)閚otify函數(shù)處理的是多個(gè)而非單一監(jiān)聽器。

這種命名方式會(huì)顯得冗長(zhǎng),而且通常一個(gè)subject會(huì)注冊(cè)多個(gè)類型的監(jiān)聽器,如前面提到的動(dòng)物園的例子,Zoo內(nèi)除了注冊(cè)監(jiān)聽動(dòng)物新增的監(jiān)聽器,還需注冊(cè)監(jiān)聽動(dòng)物減少監(jiān)聽器,此時(shí)就會(huì)有兩種register函數(shù):(registerAnimalAddedListener和 registerAnimalRemovedListener,這種方式處理,監(jiān)聽器的類型作為一個(gè)限定符,表示其應(yīng)觀察者的類型。另一解決方案是創(chuàng)建一個(gè)registerListener函數(shù)然后重載,但是方案一能更方便的知道哪個(gè)監(jiān)聽器正在監(jiān)聽,重載是比較小眾的做法。

另一慣用語法是用on前綴而不是update,例如update函數(shù)命名為onAnimalAdded而不是updateAnimalAdded。這種情況在監(jiān)聽器獲得一個(gè)序列的通知時(shí)更常見,如向list中新增一個(gè)動(dòng)物,但很少用于更新一個(gè)單獨(dú)的數(shù)據(jù),比如動(dòng)物的名字。

接下來本文將使用Java的符號(hào)規(guī)則,雖然符號(hào)規(guī)則不會(huì)改變系統(tǒng)的真實(shí)設(shè)計(jì)和實(shí)現(xiàn),但是使用其他開發(fā)者都熟悉的術(shù)語是很重要的開發(fā)準(zhǔn)則,因此要熟悉上文描述的Java中的觀察者模式符號(hào)規(guī)則。下文將在Java8環(huán)境下用一個(gè)簡(jiǎn)單例子來闡述上述概念。

一個(gè)簡(jiǎn)單的實(shí)例
還是前面提到的動(dòng)物園的例子,使用Java8的API接口實(shí)現(xiàn)一個(gè)簡(jiǎn)單的系統(tǒng),說明觀察者模式的基本原理。問題描述為:

創(chuàng)建一個(gè)系統(tǒng)zoo,允許用戶監(jiān)聽和撤銷監(jiān)聽添加新對(duì)象animal的狀態(tài),另外再創(chuàng)建一個(gè)具體監(jiān)聽器,負(fù)責(zé)輸出新增動(dòng)物的name。

根據(jù)前面對(duì)觀察者模式的學(xué)習(xí)知道實(shí)現(xiàn)這樣的應(yīng)用需要?jiǎng)?chuàng)建4個(gè)類,具體是:

  • Zoo類:即模式中的主題,負(fù)責(zé)存儲(chǔ)動(dòng)物園中的所有動(dòng)物,并在新動(dòng)物加入時(shí)通知所有已注冊(cè)的監(jiān)聽器。
  • Animal類:代表動(dòng)物對(duì)象。
  • AnimalAddedListener類:即觀察者接口。
  • PrintNameAnimalAddedListener:具體的觀察者類,負(fù)責(zé)輸出新增動(dòng)物的name。

首先我們創(chuàng)建一個(gè)Animal類,它是一個(gè)包含name成員變量、構(gòu)造函數(shù)、getter和setter方法的簡(jiǎn)單Java對(duì)象,代碼如下:

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;
  }
}

用這個(gè)類代表動(dòng)物對(duì)象,接下來就可以創(chuàng)建AnimalAddedListener接口了:

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

前面兩個(gè)類很簡(jiǎn)單,就不再詳細(xì)介紹,接下來創(chuàng)建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));
  }
}

這個(gè)類比前面兩個(gè)都復(fù)雜,其包含兩個(gè)list,一個(gè)用來存儲(chǔ)動(dòng)物園中所有動(dòng)物,另一個(gè)用來存儲(chǔ)所有的監(jiān)聽器,鑒于animals和listener集合存儲(chǔ)的對(duì)象都很簡(jiǎn)單,本文選擇了ArrayList來存儲(chǔ)。存儲(chǔ)監(jiān)聽器的具體數(shù)據(jù)結(jié)構(gòu)要視問題而定,比如對(duì)于這里的動(dòng)物園問題,如果監(jiān)聽器有優(yōu)先級(jí),那就應(yīng)該選擇其他的數(shù)據(jù)結(jié)構(gòu),或者重寫監(jiān)聽器的register算法。

注冊(cè)和移除的實(shí)現(xiàn)都是簡(jiǎn)單的委托方式:各個(gè)監(jiān)聽器作為參數(shù)從監(jiān)聽者的監(jiān)聽列表增加或者移除。notify函數(shù)的實(shí)現(xiàn)與觀察者模式的標(biāo)準(zhǔn)格式稍微偏離,它包括輸入?yún)?shù):新增加的animal,這樣一來notify函數(shù)就可以把新增加的animal引用傳遞給監(jiān)聽器了。用streams API的forEach函數(shù)遍歷監(jiān)聽器,對(duì)每個(gè)監(jiān)聽器執(zhí)行theonAnimalAdded函數(shù)。

在addAnimal函數(shù)中,新增的animal對(duì)象和監(jiān)聽器各自添加到對(duì)應(yīng)list。如果不考慮通知過程的復(fù)雜性,這一邏輯應(yīng)包含在方便調(diào)用的方法中,只需要傳入指向新增animal對(duì)象的引用即可,這就是通知監(jiān)聽器的邏輯實(shí)現(xiàn)封裝在notifyAnimalAddedListeners函數(shù)中的原因,這一點(diǎn)在addAnimal的實(shí)現(xiàn)中也提到過。

除了notify函數(shù)的邏輯問題,需要強(qiáng)調(diào)一下對(duì)notify函數(shù)可見性的爭(zhēng)議問題。在經(jīng)典的觀察者模型中,如GoF在設(shè)計(jì)模式一書中第301頁所說,notify函數(shù)是public型的,然而盡管在經(jīng)典模式中用到,這并不意味著必須是public的。選擇可見性應(yīng)該基于應(yīng)用,例如本文的動(dòng)物園的例子,notify函數(shù)是protected類型,并不要求每個(gè)對(duì)象都可以發(fā)起一個(gè)注冊(cè)觀察者的通知,只需保證對(duì)象能從父類繼承該功能即可。當(dāng)然,也并非完全如此,需要弄清楚哪些類可以激活notify函數(shù),然后再由此確定函數(shù)的可見性。

接下來需要實(shí)現(xiàn)PrintNameAnimalAddedListener類,這個(gè)類用System.out.println方法將新增動(dòng)物的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() + "'");
  }
}

最后要實(shí)現(xiàn)驅(qū)動(dòng)應(yīng)用的主函數(shù):

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"));
  }
}

主函數(shù)只是簡(jiǎn)單的創(chuàng)建了一個(gè)zoo對(duì)象,注冊(cè)了一個(gè)輸出動(dòng)物name的監(jiān)聽器,并新建了一個(gè)animal對(duì)象以觸發(fā)已注冊(cè)的監(jiān)聽器,最后的輸出為:

Added a new animal with name 'Tiger'

新增監(jiān)聽器
當(dāng)監(jiān)聽器重新建立并將其添加到Subject時(shí),觀察者模式的優(yōu)勢(shì)就充分顯示出來。例如,想添加一個(gè)計(jì)算動(dòng)物園中動(dòng)物總數(shù)的監(jiān)聽器,只需要新建一個(gè)具體的監(jiān)聽器類并注冊(cè)到Zoo類即可,而無需對(duì)zoo類做任何修改。添加計(jì)數(shù)監(jiān)聽器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函數(shù)如下:

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"));
  }
}

輸出結(jié)果為:

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

使用者可在僅修改監(jiān)聽器注冊(cè)代碼的情況下,創(chuàng)建任意監(jiān)聽器。具有此可擴(kuò)展性主要是因?yàn)镾ubject和觀察者接口關(guān)聯(lián),而不是直接和ConcreteObserver關(guān)聯(lián)。只要接口不被修改,調(diào)用接口的Subject就無需修改。

匿名內(nèi)部類,Lambda函數(shù)和監(jiān)聽器注冊(cè)
Java8的一大改進(jìn)是增加了功能特性,如增加了lambda函數(shù)。在引進(jìn)lambda函數(shù)之前,Java通過匿名內(nèi)部類提供了類似的功能,這些類在很多已有的應(yīng)用中仍在使用。在觀察者模式下,隨時(shí)可以創(chuàng)建新的監(jiān)聽器而無需創(chuàng)建具體觀察者類,例如,PrintNameAnimalAddedListener類可以在main函數(shù)中用匿名內(nèi)部類實(shí)現(xiàn),具體實(shí)現(xiàn)代碼如下:

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函數(shù)也可以用以完成此類任務(wù):

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函數(shù)僅適用于監(jiān)聽器接口只有一個(gè)函數(shù)的情況,這個(gè)要求雖然看起來嚴(yán)格,但實(shí)際上很多監(jiān)聽器都是單一函數(shù)的,如示例中的AnimalAddedListener。如果接口有多個(gè)函數(shù),可以選擇使用匿名內(nèi)部類。

隱式注冊(cè)創(chuàng)建的監(jiān)聽器存在此類問題:由于對(duì)象是在注冊(cè)調(diào)用的范圍內(nèi)創(chuàng)建的,所以不可能將引用存儲(chǔ)一個(gè)到具體監(jiān)聽器。這意味著,通過lambda函數(shù)或者匿名內(nèi)部類注冊(cè)的監(jiān)聽器不可以撤銷注冊(cè),因?yàn)槌蜂N函數(shù)需要傳入已經(jīng)注冊(cè)監(jiān)聽器的引用。解決這個(gè)問題的一個(gè)簡(jiǎn)單方法是在registerAnimalAddedListener函數(shù)中返回注冊(cè)監(jiān)聽器的引用。如此一來,就可以撤銷注冊(cè)用lambda函數(shù)或匿名內(nèi)部類創(chuàng)建的監(jiān)聽器,改進(jìn)后的方法代碼如下:

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

重新設(shè)計(jì)的函數(shù)交互的客戶端代碼如下:

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"));
  }
}

此時(shí)的結(jié)果輸出只有Added a new animal with name ‘Tiger',因?yàn)樵诘诙€(gè)animal加入之前監(jiān)聽器已經(jīng)撤銷了:

Added a new animal with name 'Tiger'
如果采用更復(fù)雜的解決方案,register函數(shù)也可以返回receipt類,以便unregister監(jiān)聽器調(diào)用,例如:

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

receipt會(huì)作為注冊(cè)函數(shù)的返回值,以及撤銷注冊(cè)函數(shù)輸入?yún)?shù),此時(shí)的zoo實(shí)現(xiàn)如下所示:

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...
}

上面描述的接收實(shí)現(xiàn)機(jī)制允許保存信息供監(jiān)聽器撤銷時(shí)調(diào)用的,也就是說如果撤銷注冊(cè)算法依賴于Subject注冊(cè)監(jiān)聽器時(shí)的狀態(tài),則此狀態(tài)將被保存,如果撤銷注冊(cè)只需要指向之前注冊(cè)監(jiān)聽器的引用,這樣的話接收技術(shù)則顯得麻煩,不推薦使用。

除了特別復(fù)雜的具體監(jiān)聽器,最常見的注冊(cè)監(jiān)聽器的方法是通過lambda函數(shù)或通過匿名內(nèi)部類注冊(cè)。當(dāng)然,也有例外,那就是包含subject實(shí)現(xiàn)觀察者接口的類和注冊(cè)一個(gè)包含調(diào)用該引用目標(biāo)的監(jiān)聽器。如下面代碼所示的案例:

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"));
  }
}

這種方法只適用于簡(jiǎn)單情況而且代碼看起來不夠?qū)I(yè),盡管如此,它還是深受現(xiàn)代Java開發(fā)人員的喜愛,因此了解這個(gè)例子的工作原理很有必要。因?yàn)閆ooContainer實(shí)現(xiàn)了AnimalAddedListener接口,那么ZooContainer的實(shí)例(或者說對(duì)象)就可以注冊(cè)為AnimalAddedListener。ZooContainer類中,該引用代表當(dāng)前對(duì)象即ZooContainer的一個(gè)實(shí)例,所以可以被用作AnimalAddedListener。

通常,不是要求所有的container類都實(shí)現(xiàn)此類功能,而且實(shí)現(xiàn)監(jiān)聽器接口的container類只能調(diào)用Subject的注冊(cè)函數(shù),只是簡(jiǎn)單把該引用作為監(jiān)聽器的對(duì)象傳給register函數(shù)。在接下來的章節(jié)中,將介紹多線程環(huán)境的常見問題和解決方案。

線程安全的實(shí)現(xiàn)
前面章節(jié)介紹了在現(xiàn)代Java環(huán)境下的實(shí)現(xiàn)觀察者模式,雖然簡(jiǎn)單但很完整,但這一實(shí)現(xiàn)忽略了一個(gè)關(guān)鍵性問題:線程安全。大多數(shù)開放的Java應(yīng)用都是多線程的,而且觀察者模式也多用于多線程或異步系統(tǒng)。例如,如果外部服務(wù)更新其數(shù)據(jù)庫,那么應(yīng)用也會(huì)異步地收到消息,然后用觀察者模式通知內(nèi)部組件更新,而不是內(nèi)部組件直接注冊(cè)監(jiān)聽外部服務(wù)。

觀察者模式的線程安全主要集中在模式的主體上,因?yàn)樾薷淖?cè)監(jiān)聽器集合時(shí)很可能發(fā)生線程沖突,比如,一個(gè)線程試圖添加一個(gè)新的監(jiān)聽器,而另一線程又試圖添加一個(gè)新的animal對(duì)象,這將觸發(fā)對(duì)所有注冊(cè)監(jiān)聽器的通知。鑒于先后順序,在已注冊(cè)的監(jiān)聽器收到新增動(dòng)物的通知前,第一個(gè)線程可能已經(jīng)完成也可能尚未完成新監(jiān)聽器的注冊(cè)。這是一個(gè)經(jīng)典的線程資源競(jìng)爭(zhēng)案例,正是這一現(xiàn)象告訴開發(fā)者們需要一個(gè)機(jī)制來保證線程安全。

這一問題的最簡(jiǎn)單的解決方案是:所有訪問或修改注冊(cè)監(jiān)聽器list的操作都須遵循Java的同步機(jī)制,比如:

public synchronized AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }
public synchronized void unregisterAnimalAddedListener (AnimalAddedListener listener) { /*...*/ }
public synchronized void notifyAnimalAddedListeners (Animal animal) { /*...*/ }

這樣一來,同一時(shí)刻只有一個(gè)線程可以修改或訪問已注冊(cè)的監(jiān)聽器列表,可以成功地避免資源競(jìng)爭(zhēng)問題,但是新問題又出現(xiàn)了,這樣的約束太過嚴(yán)格(synchronized關(guān)鍵字和Java并發(fā)模型的更多信息,請(qǐng)參閱官方網(wǎng)頁)。通過方法同步,可以時(shí)刻觀測(cè)對(duì)監(jiān)聽器list的并發(fā)訪問,注冊(cè)和撤銷監(jiān)聽器對(duì)監(jiān)聽器list而言是寫操作,而通知監(jiān)聽器訪問監(jiān)聽器list是只讀操作。由于通過通知訪問是讀操作,因此是可以多個(gè)通知操作同時(shí)進(jìn)行的。

因此,只要沒有監(jiān)聽器注冊(cè)或撤銷注冊(cè),任意多的并發(fā)通知都可以同時(shí)執(zhí)行,而不會(huì)引發(fā)對(duì)注冊(cè)的監(jiān)聽器列表的資源爭(zhēng)奪。當(dāng)然,其他情況下的資源爭(zhēng)奪現(xiàn)象存在已久,為了解決這一問題,設(shè)計(jì)了ReadWriteLock用以分開管理讀寫操作的資源鎖定。Zoo類的線程安全ThreadSafeZoo實(shí)現(xiàn)代碼如下:

public class ThreadSafeZoo {
  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  protected final Lock readLock = readWriteLock.readLock();
  protected final Lock writeLock = readWriteLock.writeLock();
  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 AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {
    // Lock the list of listeners for writing
    this.writeLock.lock();
    try {
      // Add the listener to the list of registered listeners
      this.listeners.add(listener);
    }
    finally {
      // Unlock the writer lock
      this.writeLock.unlock();
    }
    return listener;
  }
  public void unregisterAnimalAddedListener (AnimalAddedListener listener) {
    // Lock the list of listeners for writing
    this.writeLock.lock();
    try {
      // Remove the listener from the list of the registered listeners
      this.listeners.remove(listener);
    }
    finally {
      // Unlock the writer lock
      this.writeLock.unlock();
    }
  }
  public void notifyAnimalAddedListeners (Animal animal) {
    // Lock the list of listeners for reading
    this.readLock.lock();
    try {
      // Notify each of the listeners in the list of registered listeners
      this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
    }
    finally {
      // Unlock the reader lock
      this.readLock.unlock();
    }
  }
}

通過這樣部署,Subject的實(shí)現(xiàn)能確保線程安全并且多個(gè)線程可以同時(shí)發(fā)布通知。但盡管如此,依舊存在兩個(gè)不容忽略的資源競(jìng)爭(zhēng)問題:

對(duì)每個(gè)監(jiān)聽器的并發(fā)訪問。多個(gè)線程可以同時(shí)通知監(jiān)聽器要新增動(dòng)物了,這意味著一個(gè)監(jiān)聽器可能會(huì)同時(shí)被多個(gè)線程同時(shí)調(diào)用。

對(duì)animal list的并發(fā)訪問。多個(gè)線程可能會(huì)同時(shí)向animal list添加對(duì)象,如果通知的先后順序存在影響,那就可能導(dǎo)致資源競(jìng)爭(zhēng),這就需要一個(gè)并發(fā)操作處理機(jī)制來避免這一問題。如果注冊(cè)的監(jiān)聽器列表在收到通知添加animal2后,又收到通知添加animal1,此時(shí)就會(huì)產(chǎn)生資源競(jìng)爭(zhēng)。但是如果animal1和animal2的添加由不同的線程執(zhí)行,也是有可能在animal2前完成對(duì)animal1添加操作,具體來說就是線程1在通知監(jiān)聽器前添加animal1并鎖定模塊,線程2添加animal2并通知監(jiān)聽器,然后線程1通知監(jiān)聽器animal1已經(jīng)添加。雖然在不考慮先后順序時(shí),可以忽略資源競(jìng)爭(zhēng),但問題是真實(shí)存在的。

對(duì)監(jiān)聽器的并發(fā)訪問
并發(fā)訪問監(jiān)聽器可以通過保證監(jiān)聽器的線程安全來實(shí)現(xiàn)。秉承著類的“責(zé)任自負(fù)”精神,監(jiān)聽器有“義務(wù)”確保自身的線程安全。例如,對(duì)于前面計(jì)數(shù)的監(jiān)聽器,多線程的遞增或遞減動(dòng)物數(shù)量可能導(dǎo)致線程安全問題,要避免這一問題,動(dòng)物數(shù)的計(jì)算必須是原子操作(原子變量或方法同步),具體解決代碼如下:

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

方法同步解決方案代碼如下:

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

要強(qiáng)調(diào)的是監(jiān)聽器應(yīng)該保證自身的線程安全,subject需要理解監(jiān)聽器的內(nèi)部邏輯,而不是簡(jiǎn)單確保對(duì)監(jiān)聽器的訪問和修改的線程安全。否則,如果多個(gè)subject共用同一個(gè)監(jiān)聽器,那每個(gè)subject類都要重寫一遍線程安全的代碼,顯然這樣的代碼不夠簡(jiǎn)潔,因此需要在監(jiān)聽器類內(nèi)實(shí)現(xiàn)線程安全。

監(jiān)聽器的有序通知
當(dāng)要求監(jiān)聽器有序執(zhí)行時(shí),讀寫鎖就不能滿足需求了,而需要引入一個(gè)新的機(jī)制,可以保證notify函數(shù)的調(diào)用順序和animal添加到zoo的順序一致。有人嘗試過用方法同步來實(shí)現(xiàn),然而根據(jù)Oracle文檔中的方法同步介紹,可知方法同步并不提供操作執(zhí)行的順序管理。它只是保證原子操作,也就是說操作不會(huì)被打斷,并不能保證先來先執(zhí)行(FIFO)的線程順序。ReentrantReadWriteLock可以實(shí)現(xiàn)這樣的執(zhí)行順序,代碼如下:

public class OrderedThreadSafeZoo {
  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
  protected final Lock readLock = readWriteLock.readLock();
  protected final Lock writeLock = readWriteLock.writeLock();
  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 AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {
    // Lock the list of listeners for writing
    this.writeLock.lock();
    try {
      // Add the listener to the list of registered listeners
      this.listeners.add(listener);
    }
    finally {
      // Unlock the writer lock
      this.writeLock.unlock();
    }
    return listener;
  }
  public void unregisterAnimalAddedListener (AnimalAddedListener listener) {
    // Lock the list of listeners for writing
    this.writeLock.lock();
    try {
      // Remove the listener from the list of the registered listeners
      this.listeners.remove(listener);
    }
    finally {
      // Unlock the writer lock
      this.writeLock.unlock();
    }
  }
  public void notifyAnimalAddedListeners (Animal animal) {
    // Lock the list of listeners for reading
    this.readLock.lock();
    try {
      // Notify each of the listeners in the list of registered listeners
      this.listeners.forEach(listener -> listener.updateAnimalAdded(animal));
    }
    finally {
      // Unlock the reader lock
      this.readLock.unlock();
    }
  }
}

這樣的實(shí)現(xiàn)方式,register, unregister和notify函數(shù)將按照先進(jìn)先出(FIFO)的順序獲得讀寫鎖權(quán)限。例如,線程1注冊(cè)一個(gè)監(jiān)聽器,線程2在開始執(zhí)行注冊(cè)操作后試圖通知已注冊(cè)的監(jiān)聽器,線程3在線程2等待只讀鎖的時(shí)候也試圖通知已注冊(cè)的監(jiān)聽器,采用fair-ordering方式,線程1先完成注冊(cè)操作,然后線程2可以通知監(jiān)聽器,最后線程3通知監(jiān)聽器。這樣保證了action的執(zhí)行順序和開始順序一致。

如果采用方法同步,雖然線程2先排隊(duì)等待占用資源,線程3仍可能比線程2先獲得資源鎖,而且不能保證線程2比線程3先通知監(jiān)聽器。問題的關(guān)鍵所在:fair-ordering方式可以保證線程按照申請(qǐng)資源的順序執(zhí)行。讀寫鎖的順序機(jī)制很復(fù)雜,應(yīng)參照ReentrantReadWriteLock的官方文檔以確保鎖的邏輯足夠解決問題。

截止目前實(shí)現(xiàn)了線程安全,在接下來的章節(jié)中將介紹提取主題的邏輯并將其mixin類封裝為可重復(fù)代碼單元的方式優(yōu)缺點(diǎn)。

主題邏輯封裝到Mixin類
把上述的觀察者模式設(shè)計(jì)實(shí)現(xiàn)封裝到目標(biāo)的mixin類中很具吸引力。通常來說,觀察者模式中的觀察者包含已注冊(cè)的監(jiān)聽器的集合;負(fù)責(zé)注冊(cè)新的監(jiān)聽器的register函數(shù);負(fù)責(zé)撤銷注冊(cè)的unregister函數(shù)和負(fù)責(zé)通知監(jiān)聽器的notify函數(shù)。對(duì)于上述的動(dòng)物園的例子,zoo類除動(dòng)物列表是問題所需外,其他所有操作都是為了實(shí)現(xiàn)主題的邏輯。

Mixin類的案例如下所示,需要說明的是為使代碼更為簡(jiǎn)潔,此處去掉關(guān)于線程安全的代碼:

public abstract class ObservableSubjectMixin<ListenerType> {
  private List<ListenerType> listeners = new ArrayList<>();
  public ListenerType registerListener (ListenerType listener) {
    // Add the listener to the list of registered listeners
    this.listeners.add(listener);
    return listener;
  }
  public void unregisterAnimalAddedListener (ListenerType listener) {
    // Remove the listener from the list of the registered listeners
    this.listeners.remove(listener);
  }
  public void notifyListeners (Consumer<? super ListenerType> algorithm) {
    // Execute some function on each of the listeners
    this.listeners.forEach(algorithm);
  }
}

正因?yàn)闆]有提供正在注冊(cè)的監(jiān)聽器類型的接口信息,不能直接通知某個(gè)特定的監(jiān)聽器,所以正需要保證通知功能的通用性,允許客戶端添加一些功能,如接受泛型參數(shù)類型的參數(shù)匹配,以適用于每個(gè)監(jiān)聽器,具體實(shí)現(xiàn)代碼如下:

public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> {
  private List<Animal> animals = 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.notifyListeners((listener) -> listener.updateAnimalAdded(animal));
  }
}

Mixin類技術(shù)的最大優(yōu)勢(shì)是把觀察者模式的Subject封裝到一個(gè)可重復(fù)調(diào)用的類中,而不是在每個(gè)subject類中都重復(fù)寫這些邏輯。此外,這一方法使得zoo類的實(shí)現(xiàn)更為簡(jiǎn)潔,只需要存儲(chǔ)動(dòng)物信息,而不用再考慮如何存儲(chǔ)和通知監(jiān)聽器。

然而,使用mixin類并非只有優(yōu)點(diǎn)。比如,如果要存儲(chǔ)多個(gè)類型的監(jiān)聽器怎么辦?例如,還需要存儲(chǔ)監(jiān)聽器類型AnimalRemovedListener。mixin類是抽象類,Java中不能同時(shí)繼承多個(gè)抽象類,而且mixin類不能改用接口實(shí)現(xiàn),這是因?yàn)榻涌诓话瑂tate,而觀察者模式中state需要用來保存已經(jīng)注冊(cè)的監(jiān)聽器列表。

其中的一個(gè)解決方案是創(chuàng)建一個(gè)動(dòng)物增加和減少時(shí)都會(huì)通知的監(jiān)聽器類型ZooListener,代碼如下所示:

public interface ZooListener {
  public void onAnimalAdded (Animal animal);
  public void onAnimalRemoved (Animal animal);
}

這樣就可以使用該接口實(shí)現(xiàn)利用一個(gè)監(jiān)聽器類型對(duì)zoo狀態(tài)各種變化的監(jiān)聽了:

public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> {
  private List<Animal> animals = 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.notifyListeners((listener) -> listener.onAnimalAdded(animal));
  }
  public void removeAnimal (Animal animal) {
    // Remove the animal from the list of animals
    this.animals.remove(animal);
    // Notify the list of registered listeners
    this.notifyListeners((listener) -> listener.onAnimalRemoved(animal));
  }
}

將多個(gè)監(jiān)聽器類型合并到一個(gè)監(jiān)聽器接口中確實(shí)解決了上面提到的問題,但仍舊存在不足之處,接下來的章節(jié)會(huì)詳細(xì)討論。

Multi-Method監(jiān)聽器和適配器
在上述方法,監(jiān)聽器的接口中實(shí)現(xiàn)的包含太多函數(shù),接口就過于冗長(zhǎng),例如,Swing MouseListener就包含5個(gè)必要的函數(shù)。盡管可能只會(huì)用到其中一個(gè),但是只要用到鼠標(biāo)點(diǎn)擊事件就必須要添加這5個(gè)函數(shù),更多可能是用空函數(shù)體來實(shí)現(xiàn)剩下的函數(shù),這無疑會(huì)給代碼帶來不必要的混亂。

其中一種解決方案是創(chuàng)建適配器(概念來自GoF提出的適配器模式),適配器中以抽象函數(shù)的形式實(shí)現(xiàn)監(jiān)聽器接口的操作,供具體監(jiān)聽器類繼承。這樣一來,具體監(jiān)聽器類就可以選擇其需要的函數(shù),對(duì)adapter不需要的函數(shù)采用默認(rèn)操作即可。例如上面例子中的ZooListener類,創(chuàng)建ZooAdapter(Adapter的命名規(guī)則與監(jiān)聽器一致,只需要把類名中的Listener改為Adapter即可),代碼如下:

public class ZooAdapter implements ZooListener {
  @Override
  public void onAnimalAdded (Animal animal) {}
  @Override
  public void onAnimalRemoved (Animal animal) {}
}

乍一看,這個(gè)適配器類微不足道,然而它所帶來的便利卻是不可小覷的。比如對(duì)于下面的具體類,只需選擇對(duì)其實(shí)現(xiàn)有用的函數(shù)即可:

public class NamePrinterZooAdapter extends ZooAdapter {
  @Override
  public void onAnimalAdded (Animal animal) {
    // Print the name of the animal that was added
    System.out.println("Added animal named " + animal.getName());
  }
}

有兩種替代方案同樣可以實(shí)現(xiàn)適配器類的功能:一是使用默認(rèn)函數(shù);二是把監(jiān)聽器接口和適配器類合并到一個(gè)具體類中。默認(rèn)函數(shù)是Java8新提出的,在接口中允許開發(fā)者提供默認(rèn)(防御)的實(shí)現(xiàn)方法。

Java庫的這一更新主要是方便開發(fā)者在不改變老版本代碼的情況下,實(shí)現(xiàn)程序擴(kuò)展,因此應(yīng)該慎用這個(gè)方法。部分開發(fā)者多次使用后,會(huì)感覺這樣寫的代碼不夠?qū)I(yè),而又有開發(fā)者認(rèn)為這是Java8的特色,不管怎樣,需要明白這個(gè)技術(shù)提出的初衷是什么,再結(jié)合具體問題決定是否要用。使用默認(rèn)函數(shù)實(shí)現(xiàn)的ZooListener接口代碼如下示:

public interface ZooListener {
  default public void onAnimalAdded (Animal animal) {}
  default public void onAnimalRemoved (Animal animal) {}
}

通過使用默認(rèn)函數(shù),實(shí)現(xiàn)該接口的具體類,無需在接口中實(shí)現(xiàn)全部函數(shù),而是選擇性實(shí)現(xiàn)所需函數(shù)。雖然這是接口膨脹問題一個(gè)較為簡(jiǎn)潔的解決方案,開發(fā)者在使用時(shí)還應(yīng)多加注意。

第二種方案是簡(jiǎn)化觀察者模式,省略了監(jiān)聽器接口,而是用具體類實(shí)現(xiàn)監(jiān)聽器的功能。比如ZooListener接口就變成了下面這樣:

public class ZooListener {
  public void onAnimalAdded (Animal animal) {}
  public void onAnimalRemoved (Animal animal) {}
}

這一方案簡(jiǎn)化了觀察者模式的層次結(jié)構(gòu),但它并非適用于所有情況,因?yàn)槿绻驯O(jiān)聽器接口合并到具體類中,具體監(jiān)聽器就不可以實(shí)現(xiàn)多個(gè)監(jiān)聽接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口寫在同一個(gè)具體類中,那么單獨(dú)一個(gè)具體監(jiān)聽器就不可以同時(shí)實(shí)現(xiàn)這兩個(gè)接口了。此外,監(jiān)聽器接口的意圖比具體類更顯而易見,很顯然前者就是為其他類提供接口,但后者就并非那么明顯了。

如果沒有合適的文檔說明,開發(fā)者并不會(huì)知道已經(jīng)有一個(gè)類扮演著接口的角色,實(shí)現(xiàn)了其對(duì)應(yīng)的所有函數(shù)。此外,類名不包含adapter,因?yàn)轭惒⒉贿m配于某一個(gè)接口,因此類名并沒有特別暗示此意圖。綜上所述,特定問題需要選擇特定的方法,并沒有哪個(gè)方法是萬能的。

在開始下一章前,需要特別提一下,適配器在觀察模式中很常見,尤其是在老版本的Java代碼中。Swing API正是以適配器為基礎(chǔ)實(shí)現(xiàn)的,正如很多老應(yīng)用在Java5和Java6中的觀察者模式中所使用的那樣。zoo案例中的監(jiān)聽器或許并不需要適配器,但需要了解適配器提出的目的以及其應(yīng)用,因?yàn)槲覀兛梢栽诂F(xiàn)有的代碼中對(duì)其進(jìn)行使用。下面的章節(jié),將會(huì)介紹時(shí)間復(fù)雜的監(jiān)聽器,該類監(jiān)聽器可能會(huì)執(zhí)行耗時(shí)的運(yùn)算或進(jìn)行異步調(diào)用,不能立即給出返回值。

Complex & Blocking監(jiān)聽器
關(guān)于觀察者模式的一個(gè)假設(shè)是:執(zhí)行一個(gè)函數(shù)時(shí),一系列監(jiān)聽器會(huì)被調(diào)用,但假定這一過程對(duì)調(diào)用者而言是完全透明的。例如,客戶端代碼在Zoo中添加animal時(shí),在返回添加成功之前,并不知道會(huì)調(diào)用一系列監(jiān)聽器。如果監(jiān)聽器的執(zhí)行需要時(shí)間較長(zhǎng)(其時(shí)間受監(jiān)聽器的數(shù)量、每個(gè)監(jiān)聽器執(zhí)行時(shí)間影響),那么客戶端代碼將會(huì)感知這一簡(jiǎn)單增加動(dòng)物操作的時(shí)間副作用。

本文不能面面俱到的討論這個(gè)話題,下面幾條是開發(fā)者調(diào)用復(fù)雜的監(jiān)聽器時(shí)應(yīng)該注意的事項(xiàng):

監(jiān)聽器啟動(dòng)新線程。新線程啟動(dòng)后,在新線程中執(zhí)行監(jiān)聽器邏輯的同時(shí),返回監(jiān)聽器函數(shù)的處理結(jié)果,并運(yùn)行其他監(jiān)聽器執(zhí)行。

Subject啟動(dòng)新線程。與傳統(tǒng)的線性迭代已注冊(cè)的監(jiān)聽器列表不同,Subject的notify函數(shù)重啟一個(gè)新的線程,然后在新線程中迭代監(jiān)聽器列表。這樣使得notify函數(shù)在執(zhí)行其他監(jiān)聽器操作的同時(shí)可以輸出其返回值。需要注意的是需要一個(gè)線程安全機(jī)制來確保監(jiān)聽器列表不會(huì)進(jìn)行并發(fā)修改。

隊(duì)列化監(jiān)聽器調(diào)用并采用一組線程執(zhí)行監(jiān)聽功能。將監(jiān)聽器操作封裝在一些函數(shù)中并隊(duì)列化這些函數(shù),而非簡(jiǎn)單的迭代調(diào)用監(jiān)聽器列表。這些監(jiān)聽器存儲(chǔ)到隊(duì)列中后,線程就可以從隊(duì)列中彈出單個(gè)元素并執(zhí)行其監(jiān)聽邏輯。這類似于生產(chǎn)者-消費(fèi)者問題,notify過程產(chǎn)生可執(zhí)行函數(shù)隊(duì)列,然后線程依次從隊(duì)列中取出并執(zhí)行這些函數(shù),函數(shù)需要存儲(chǔ)被創(chuàng)建的時(shí)間而非執(zhí)行的時(shí)間供監(jiān)聽器函數(shù)調(diào)用。例如,監(jiān)聽器被調(diào)用時(shí)創(chuàng)建的函數(shù),那么該函數(shù)就需要存儲(chǔ)該時(shí)間點(diǎn),這一功能類似于Java中的如下操作:

public class   

如何使用Java8 實(shí)現(xiàn)觀察者模式?相信通過這篇文章大家都有了大概的了解了吧!

相關(guān)文章

  • java連接mongoDB并進(jìn)行增刪改查操作實(shí)例詳解

    java連接mongoDB并進(jìn)行增刪改查操作實(shí)例詳解

    這篇文章主要介紹了java連接mongoDB并進(jìn)行增刪改查操作,結(jié)合實(shí)例形式詳細(xì)分析了java環(huán)境下MongoDB擴(kuò)展包的下載、安裝及操作MongoDB連接、增刪改查等相關(guān)操作技巧,需要的朋友可以參考下
    2019-04-04
  • java ExecutorService使用方法詳解

    java ExecutorService使用方法詳解

    這篇文章主要為大家詳細(xì)介紹了java ExecutorService使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • 阿里Sentinel支持Spring Cloud Gateway的實(shí)現(xiàn)

    阿里Sentinel支持Spring Cloud Gateway的實(shí)現(xiàn)

    這篇文章主要介紹了阿里Sentinel支持Spring Cloud Gateway的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-04-04
  • jdbc實(shí)現(xiàn)用戶注冊(cè)功能代碼示例

    jdbc實(shí)現(xiàn)用戶注冊(cè)功能代碼示例

    這篇文章主要介紹了jdbc實(shí)現(xiàn)用戶注冊(cè)功能,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下
    2018-01-01
  • Java StackOverflowError詳解

    Java StackOverflowError詳解

    這篇文章主要介紹了Java StackOverflowError詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • java面試常見模式問題---單例模式

    java面試常見模式問題---單例模式

    單例模式(Singleton Pattern)是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式
    2021-06-06
  • Java稀疏數(shù)組的應(yīng)用實(shí)踐

    Java稀疏數(shù)組的應(yīng)用實(shí)踐

    本文主要介紹了Java稀疏數(shù)組的應(yīng)用實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • Java Calendar類使用案例詳解

    Java Calendar類使用案例詳解

    這篇文章主要介紹了Java Calendar類使用案例詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • jackson 實(shí)體轉(zhuǎn)json 為NULL或者為空不參加序列化(實(shí)例講解)

    jackson 實(shí)體轉(zhuǎn)json 為NULL或者為空不參加序列化(實(shí)例講解)

    下面小編就為大家?guī)硪黄猨ackson 實(shí)體轉(zhuǎn)json 為NULL或者為空不參加序列化(實(shí)例講解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-10-10
  • springboot如何讀取自定義properties并注入到bean中

    springboot如何讀取自定義properties并注入到bean中

    這篇文章主要介紹了springboot讀取自定義properties并注入到bean中,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11

最新評(píng)論