設計模式(六)觀察者模式詳解(包含觀察者模式JDK的漏洞以及事件驅動模型)

楓奇發表於2017-06-30

 

 轉載自  http://blog.csdn.net/zuoxiaolong8810/article/details/9081079

    作者:zuoxiaolong8810(左瀟龍),轉載請註明出處。


 本章我們討論一個除前面的單例以及代理模式之外,一個WEB專案中有可能用到的設計模式,即觀察者模式。

                 說起觀察者模式,LZ還是非常激動的,當初這算是第一個讓LZ感受到設計模式強大的傢伙。當初LZ要做一個小型WEB專案,要上傳給伺服器檔案,一個需求就是要顯示上傳進度,LZ就是用這個模式解決了當時的問題,那時LZ接觸Java剛幾個月,比葫蘆畫瓢的用了觀察者模式。

                 現在談及觀察者模式,能用到的地方就相對較多了,通常意義上如果一個物件狀態的改變需要通知很多對這個物件關注的一系列物件,就可以使用觀察者模式。

                 下面LZ先給出觀察者模式標準版的定義,引自百度百科。

                 定義:觀察者模式(有時又被稱為釋出-訂閱模式、模型-檢視模式、源-收聽者模式或從屬者模式)是軟體設計模式的一種。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實作事件處理系統。

                 上面的定義當中,主要有這樣幾個意思,首先是有一個目標的物件,通俗點講就是一個類,它管理了所有依賴於它的觀察者物件,或者通俗點說是觀察者類,並在它自己狀態發生變化時,主動發出通知。

                 簡單點概括成通俗的話來說,就是一個類管理著所有依賴於它的觀察者類,並且它狀態變化時會主動給這些依賴它的類發出通知。

                 那麼我們針對上面的描述給出觀察者模式的類圖,百度百科沒有給出觀察者模式的類圖,這裡LZ自己使用工具給各位畫一個。


                 可以看到,我們的被觀察者類Observable只關聯了一個Observer的列表,然後在自己狀態變化時,使用notifyObservers方法通知這些Observer,具體這些Observer都是什麼,被觀察者是不關心也不需要知道的。

                上面就將觀察者和被觀察者二者的耦合度降到很低了,而我們具體的觀察者是必須要知道自己觀察的是誰,所以它依賴於被觀察者。

                下面LZ給寫出一個很簡單的觀察者模式,來使用JAVA程式碼簡單詮釋一下上面的類圖。

                首先是觀察者介面。

[java] view plain copy
  1. package net;  
  2.   
  3. //這個介面是為了提供一個統一的觀察者做出相應行為的方法  
  4. public interface Observer {  
  5.   
  6.     void update(Observable o);  
  7.       
  8. }  
                再者是具體的觀察者。

[java] view plain copy
  1. package net;  
  2.   
  3. public class ConcreteObserver1 implements Observer{  
  4.   
  5.     public void update(Observable o) {  
  6.         System.out.println("觀察者1觀察到" + o.getClass().getSimpleName() + "發生變化");  
  7.         System.out.println("觀察者1做出相應");  
  8.     }  
  9.   
  10. }  
[java] view plain copy
  1. package net;  
  2.   
  3. public class ConcreteObserver2 implements Observer{  
  4.   
  5.     public void update(Observable o) {  
  6.         System.out.println("觀察者2觀察到" + o.getClass().getSimpleName() + "發生變化");  
  7.         System.out.println("觀察者2做出相應");  
  8.     }  
  9.   
  10. }  

                下面是被觀察者,它有一個觀察者的列表,並且有一個通知所有觀察者的方法,通知的方式就是呼叫觀察者通用的介面行為update方法。下面我們看它的程式碼。

[java] view plain copy
  1. package net;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. public class Observable {  
  7.   
  8.     List<Observer> observers = new ArrayList<Observer>();  
  9.       
  10.     public void addObserver(Observer o){  
  11.         observers.add(o);  
  12.     }  
  13.       
  14.     public void changed(){  
  15.         System.out.println("我是被觀察者,我已經發生變化了");  
  16.         notifyObservers();//通知觀察自己的所有觀察者  
  17.     }  
  18.       
  19.     public void notifyObservers(){  
  20.         for (Observer observer : observers) {  
  21.             observer.update(this);  
  22.         }  
  23.     }  
  24. }  
                這裡面很簡單,新增兩個方法,一個是為了改變自己的同時通知觀察者們,一個是為了給客戶端一個新增觀察者的公共介面。

                下面我們使用客戶端呼叫一下,看一下客戶端如何操作。

[java] view plain copy
  1. package net;  
  2.   
  3.   
  4. public class Client {  
  5.   
  6.     public static void main(String[] args) throws Exception {  
  7.         Observable observable = new Observable();  
  8.         observable.addObserver(new ConcreteObserver1());  
  9.         observable.addObserver(new ConcreteObserver2());  
  10.           
  11.         observable.changed();  
  12.     }  
  13. }  
                 執行結果如下。

                 可以看到我們在操作被觀察者時,只要呼叫changed方法,觀察者們就會做出相應的動作,而新增觀察者這個行為算是準備階段,將具體的觀察者關聯到被觀察者上面去。 

                下面LZ給出一個有實際意義的例子,比如我們經常看的小說網站,都有這樣的功能,就是讀者可以訂閱作者,這當中就有明顯的觀察者模式案例,就是作者和讀者。他們的關係是一旦讀者關注了一個作者,那麼這個作者一旦有什麼新書,就都要通知讀者們,這明顯是一個觀察者模式的案例,所以我們可以使用觀察者模式解決。

                 由於JDK中為了方便開發人員,已經寫好了現成的觀察者介面和被觀察者類,下面LZ先給出JDK中現成的觀察者和被觀察者程式碼,外加自己的一點解釋,來幫助一些讀者對JDK中對觀察者模式的支援熟悉一下。

                 先來觀察者介面。

[java] view plain copy
  1. //觀察者介面,每一個觀察者都必須實現這個介面  
  2. public interface Observer {  
  3.     //這個方法是觀察者在觀察物件產生變化時所做的響應動作,從中傳入了觀察的物件和一個預留引數  
  4.     void update(Observable o, Object arg);  
  5.   
  6. }  

                下面是被觀察者類。

[java] view plain copy
  1. import java.util.Vector;  
  2.   
  3. //被觀察者類  
  4. public class Observable {  
  5.     //這是一個改變標識,來標記該被觀察者有沒有改變  
  6.     private boolean changed = false;  
  7.     //持有一個觀察者列表  
  8.     private Vector obs;  
  9.       
  10.     public Observable() {  
  11.     obs = new Vector();  
  12.     }  
  13.     //新增觀察者,新增時會去重  
  14.     public synchronized void addObserver(Observer o) {  
  15.         if (o == null)  
  16.             throw new NullPointerException();  
  17.     if (!obs.contains(o)) {  
  18.         obs.addElement(o);  
  19.     }  
  20.     }  
  21.     //刪除觀察者  
  22.     public synchronized void deleteObserver(Observer o) {  
  23.         obs.removeElement(o);  
  24.     }  
  25.     //notifyObservers(Object arg)的過載方法  
  26.     public void notifyObservers() {  
  27.     notifyObservers(null);  
  28.     }  
  29.     //通知所有觀察者,被觀察者改變了,你可以執行你的update方法了。  
  30.     public void notifyObservers(Object arg) {  
  31.         //一個臨時的陣列,用於併發訪問被觀察者時,留住觀察者列表的當前狀態,這種處理方式其實也算是一種設計模式,即備忘錄模式。  
  32.         Object[] arrLocal;  
  33.     //注意這個同步塊,它表示在獲取觀察者列表時,該物件是被鎖定的  
  34.     //也就是說,在我獲取到觀察者列表之前,不允許其他執行緒改變觀察者列表  
  35.     synchronized (this) {  
  36.         //如果沒變化直接返回  
  37.         if (!changed)  
  38.                 return;  
  39.             //這裡將當前的觀察者列表放入臨時陣列  
  40.             arrLocal = obs.toArray();  
  41.             //將改變標識重新置回未改變  
  42.             clearChanged();  
  43.         }  
  44.         //注意這個for迴圈沒有在同步塊,此時已經釋放了被觀察者的鎖,其他執行緒可以改變觀察者列表  
  45.         //但是這並不影響我們當前進行的操作,因為我們已經將觀察者列表複製到臨時陣列  
  46.         //在通知時我們只通知陣列中的觀察者,當前刪除和新增觀察者,都不會影響我們通知的物件  
  47.         for (int i = arrLocal.length-1; i>=0; i--)  
  48.             ((Observer)arrLocal[i]).update(this, arg);  
  49.     }  
  50.   
  51.     //刪除所有觀察者  
  52.     public synchronized void deleteObservers() {  
  53.     obs.removeAllElements();  
  54.     }  
  55.   
  56.     //標識被觀察者被改變過了  
  57.     protected synchronized void setChanged() {  
  58.     changed = true;  
  59.     }  
  60.     //標識被觀察者沒改變  
  61.     protected synchronized void clearChanged() {  
  62.     changed = false;  
  63.     }  
  64.     //返回被觀察者是否改變  
  65.     public synchronized boolean hasChanged() {  
  66.     return changed;  
  67.     }  
  68.     //返回觀察者數量  
  69.     public synchronized int countObservers() {  
  70.     return obs.size();  
  71.     }  
  72. }  

                 被觀察者除了一點同步的地方需要特殊解釋一下,其餘的相信各位都能看明白各個方法的用途。其實上述JDK的類是有漏洞的,或者說,在我們使用觀察者模式時要注意一個問題,就是notifyObservers這個方法中的這一段程式碼。

[java] view plain copy
  1. for (int i = arrLocal.length-1; i>=0; i--)  
  2.             ((Observer)arrLocal[i]).update(this, arg);  

                 在迴圈遍歷觀察者讓觀察者做出響應時,JDK沒有去抓取update方法中的異常,所以假設在這過程中有一個update方法丟擲了異常,那麼剩下還未通知的觀察者就全都通知不到了,所以LZ個人比較疑惑這樣的用意(LZ無法想象JAVA類庫的製造者沒考慮到這個問題),是sun當時真的忘了考慮這一點,還是另有它意?當然各位讀者如果有自己的見解可以告知LZ,不過LZ認為,不管是sun如此做是別有用意,還是真的欠考慮,我們都要注意在update方法裡一定要處理好異常,個人覺得JDK中比較保險的做法還是如下這樣。

[java] view plain copy
  1. for (int i = arrLocal.length-1; i>=0; i--){  
  2.             try {  
  3.                 ((Observer)arrLocal[i]).update(this, arg);  
  4.             } catch (Throwable e) {e.printStackTrace();}  
  5.         }  

                 這樣無論其中任何一個update是否成功都不會影響其餘的觀察者進行更新狀態,我們自己比較保險的做法就是給update方法整個加上try塊,或者確認不會發生執行時異常。


                 上面LZ和各位一起分析了JDK中觀察者模式的原始碼,下面我們就拿上述小說網的例子,做一個DEMO。

                 首先要搞清楚在讀者和作者之間是誰觀察誰,很明顯,應該是讀者觀察作者。所以作者是被觀察者,讀者是觀察者,除了這兩個類之外,我們還需要額外新增一個管理器幫我們管理下作者的列表便於讀者關注,於是一個觀察者模式的DEMO就出現了。如下,首先是讀者類,LZ在各個類都加了點註釋。

[java] view plain copy
  1. //讀者類,要實現觀察者介面  
  2. public class Reader implements Observer{  
  3.       
  4.     private String name;  
  5.       
  6.     public Reader(String name) {  
  7.         super();  
  8.         this.name = name;  
  9.     }  
  10.   
  11.     public String getName() {  
  12.         return name;  
  13.     }  
  14.       
  15.     //讀者可以關注某一位作者,關注則代表把自己加到作者的觀察者列表裡  
  16.     public void subscribe(String writerName){  
  17.         WriterManager.getInstance().getWriter(writerName).addObserver(this);  
  18.     }  
  19.       
  20.     //讀者可以取消關注某一位作者,取消關注則代表把自己從作者的觀察者列表裡刪除  
  21.     public void unsubscribe(String writerName){  
  22.         WriterManager.getInstance().getWriter(writerName).deleteObserver(this);  
  23.     }  
  24.       
  25.     //當關注的作者發表新小說時,會通知讀者去看  
  26.     public void update(Observable o, Object obj) {  
  27.         if (o instanceof Writer) {  
  28.             Writer writer = (Writer) o;  
  29.             System.out.println(name+"知道" + writer.getName() + "釋出了新書《" + writer.getLastNovel() + "》,非要去看!");  
  30.         }  
  31.     }  
  32.       
  33. }  

                       下面是作者類。

[java] view plain copy
  1. //作者類,要繼承自被觀察者類  
  2. public class Writer extends Observable{  
  3.       
  4.     private String name;//作者的名稱  
  5.       
  6.     private String lastNovel;//記錄作者最新發布的小說  
  7.   
  8.     public Writer(String name) {  
  9.         super();  
  10.         this.name = name;  
  11.         WriterManager.getInstance().add(this);  
  12.     }  
  13.   
  14.     //作者釋出新小說了,要通知所有關注自己的讀者  
  15.     public void addNovel(String novel) {  
  16.         System.out.println(name + "釋出了新書《" + novel + "》!");  
  17.         lastNovel = novel;  
  18.         setChanged();  
  19.         notifyObservers();  
  20.     }  
  21.       
  22.     public String getLastNovel() {  
  23.         return lastNovel;  
  24.     }  
  25.   
  26.     public String getName() {  
  27.         return name;  
  28.     }  
  29.   
  30. }  

                 然後我們還需要一個管理器幫我們管理這些作者。如下。

[java] view plain copy
  1. import java.util.HashMap;  
  2. import java.util.Map;  
  3.   
  4. //管理器,保持一份獨有的作者列表  
  5. public class WriterManager{  
  6.       
  7.     private Map<String, Writer> writerMap = new HashMap<String, Writer>();  
  8.   
  9.     //新增作者  
  10.     public void add(Writer writer){  
  11.         writerMap.put(writer.getName(), writer);  
  12.     }  
  13.     //根據作者姓名獲取作者  
  14.     public Writer getWriter(String name){  
  15.         return writerMap.get(name);  
  16.     }  
  17.       
  18.     //單例  
  19.     private WriterManager(){}  
  20.       
  21.     public static WriterManager getInstance(){  
  22.         return WriterManagerInstance.instance;  
  23.     }  
  24.     private static class WriterManagerInstance{  
  25.           
  26.         private static WriterManager instance = new WriterManager();  
  27.           
  28.     }  
  29. }  

                好了,這下我們的觀察者模式就做好了,這個簡單的DEMO可以支援讀者關注作者,當作者釋出新書時,讀者會觀察到這個事情,會產生相應的動作。下面我們寫個客戶端呼叫一下。

[java] view plain copy
  1. //客戶端呼叫  
  2. public class Client {  
  3.   
  4.     public static void main(String[] args) {  
  5.         //假設四個讀者,兩個作者  
  6.         Reader r1 = new Reader("謝廣坤");  
  7.         Reader r2 = new Reader("趙四");  
  8.         Reader r3 = new Reader("七哥");  
  9.         Reader r4 = new Reader("劉能");  
  10.         Writer w1 = new Writer("謝大腳");  
  11.         Writer w2 = new Writer("王小蒙");  
  12.         //四人關注了謝大腳  
  13.         r1.subscribe("謝大腳");  
  14.         r2.subscribe("謝大腳");  
  15.         r3.subscribe("謝大腳");  
  16.         r4.subscribe("謝大腳");  
  17.         //七哥和劉能還關注了王小蒙  
  18.         r3.subscribe("王小蒙");  
  19.         r4.subscribe("王小蒙");  
  20.           
  21.         //作者釋出新書就會通知關注的讀者  
  22.         //謝大腳寫了設計模式  
  23.         w1.addNovel("設計模式");  
  24.         //王小蒙寫了JAVA程式設計思想  
  25.         w2.addNovel("JAVA程式設計思想");  
  26.         //謝廣坤取消關注謝大腳  
  27.         r1.unsubscribe("謝大腳");  
  28.         //謝大腳再寫書將不會通知謝廣坤  
  29.         w1.addNovel("觀察者模式");  
  30.     }  
  31.       
  32. }  

                    看下我們得到的結果,就會發現,我們確實通知了讀者它所關注的作者的動態,而且讀者取消關注以後,作者的動態將不再通知該讀者。下面是執行結果。

                我們使用觀察者模式的用意是為了作者不再需要關心他釋出新書時都要去通知誰,更重要的是他不需要關心他通知的是讀者還是其它什麼人,他只知道這個人是實現了觀察者介面的,即我們的被觀察者依賴的只是一個抽象的介面觀察者介面,而不關心具體的觀察者都有誰都是什麼,比如以後要是遊客也可以關注作者了,那麼只要遊客類實現觀察者介面,那麼一樣可以將遊客列入到作者的觀察者列表中。

                另外,我們讓讀者自己來選擇自己關注的物件,這相當於被觀察者將維護通知物件的職能轉化給了觀察者,這樣做的好處是由於一個被觀察者可能有N多觀察者,所以讓被觀察者自己維護這個列表會很艱難,這就像一個老師被許多學生認識,那麼是所有的學生都記住老師的名字簡單,還是讓老師記住N多學生的名字簡單?答案顯而易見,讓學生們都記住一個老師的名字是最簡單的。

                另外,觀察者模式分離了觀察者和被觀察者二者的責任,這樣讓類之間各自維護自己的功能,專注於自己的功能,會提高系統的可維護性和可重用性。

                

                觀察者模式其實還有另外一種形態,就是事件驅動模型,LZ個人覺得這兩種方式大體上其實是非常相似的,所以LZ決定一起引入事件驅動模型。不過觀察者更多的強調的是釋出-訂閱式的問題處理,而事件驅動則更多的注重於介面與資料模型之間的問題,兩者還是有很多適用場景上的區別的,雖不能一概而論,但放在一起討論還是很方便各位理解二者。

                說到事件驅動,由於JAVA在桌面應用程式方面有很多欠缺,所以swing的使用其實並不是特別廣泛,因為你不可能要求大多數人的機子上都安裝了JDK,除非你是給特殊使用者人群開發的應用程式,這些使用者在你的可控範圍內,那麼swing或許可以派上用場。

                考慮到學習JAVA或者使用JAVA的人群大部分都是在進行web開發,所以本次討論事件驅動,採用web開發當中所用到的示例。

                相信各位都知道tomcat,這是一個app伺服器,在使用的過程中,或許經常會有人用到listener,即監聽器這個概念。那麼其實這個就是一個事件驅動模型的應用。比如我們的spring,我們在應用啟動的時候要初始化我們的IOC容器,那麼我們的做法就是加入一個listener,這樣伴隨著tomcat伺服器的啟動,spring的IOC容器就會跟著啟動。

                那麼這個listener其實就是事件驅動模型中的監聽器,它用來監聽它所感興趣的事,比如我們springIOC容器啟動的監聽器,就是實現的ServletContextListener這個介面,說明它對servletContext感興趣,會監聽servletContext的啟動和銷燬。

                LZ不打算使用這個例子作為講解,因為它的內部運作比較複雜,需要搬上來tomcat的原始碼,對於新手來說,這是個噩耗,所以我們將上述的例子改為事件驅動來實現。也好讓各位針對性的對比觀察者模式和事件驅動模型。

                首先事件驅動模型與觀察者模式勉強的對應關係可以看成是,被觀察者相當於事件源,觀察者相當於監聽器,事件源會產生事件,監聽器監聽事件。所以這其中就攙和到四個類,事件源,事件,監聽器以及具體的監聽器。

                JDK當中依然有現成的一套事件模型類庫,其中監聽器只是一個標識介面,因為它沒有表達對具體物件感興趣的意思,所以也無法定義監聽的事件,只是為了統一,用來給特定的監聽器繼承。它的原始碼如下。

[java] view plain copy
  1. package java.util;  
  2.   
  3. /** 
  4.  * A tagging interface that all event listener interfaces must extend. 
  5.  * @since JDK1.1 
  6.  */  
  7. public interface EventListener {  
  8. }  

                由於程式碼很短,所以LZ沒有刪減,當中標註了,所有的事件監聽器都必須繼承,這是一個標識介面。上述的事件,JDK當中也有一個現成的類供繼承,就是EventObject,這個類的原始碼如下。

[java] view plain copy
  1. public class EventObject implements java.io.Serializable {  
  2.   
  3.     private static final long serialVersionUID = 5516075349620653480L;  
  4.   
  5.     /** 
  6.      * The object on which the Event initially occurred. 
  7.      */  
  8.     protected transient Object  source;  
  9.   
  10.     /** 
  11.      * Constructs a prototypical Event. 
  12.      * 
  13.      * @param    source    The object on which the Event initially occurred. 
  14.      * @exception  IllegalArgumentException  if source is null. 
  15.      */  
  16.     public EventObject(Object source) {  
  17.     if (source == null)  
  18.         throw new IllegalArgumentException("null source");  
  19.   
  20.         this.source = source;  
  21.     }  
  22.   
  23.     /** 
  24.      * The object on which the Event initially occurred. 
  25.      * 
  26.      * @return   The object on which the Event initially occurred. 
  27.      */  
  28.     public Object getSource() {  
  29.         return source;  
  30.     }  
  31.   
  32.     /** 
  33.      * Returns a String representation of this EventObject. 
  34.      * 
  35.      * @return  A a String representation of this EventObject. 
  36.      */  
  37.     public String toString() {  
  38.         return getClass().getName() + "[source=" + source + "]";  
  39.     }  
  40. }  

             這個類並不複雜,它只是想表明,所有的事件都應該帶有一個事件源,大部分情況下,這個事件源就是我們被監聽的物件。

             如果我們採用事件驅動模型去分析上面的例子,那麼作者就是事件源,而讀者就是監聽器,依據這個思想,我們把上述例子改一下,首先我們需要自定義我們自己的監聽器和事件。所以我們定義如下作者事件。

[java] view plain copy
  1. import java.util.EventObject;  
  2.   
  3. public class WriterEvent extends EventObject{  
  4.       
  5.     private static final long serialVersionUID = 8546459078247503692L;  
  6.   
  7.     public WriterEvent(Writer writer) {  
  8.         super(writer);  
  9.     }  
  10.       
  11.     public Writer getWriter(){  
  12.         return (Writer) super.getSource();  
  13.     }  
  14.   
  15. }  

              這代表了一個作者事件,這個事件當中一般就是包含一個事件源,在這裡就是作者,當然有的時候你可以讓它帶有更多的資訊,以方便監聽器做出更加細緻的動作。下面我們定義如下監聽器。

[java] view plain copy
  1. import java.util.EventListener;  
  2.   
  3. public interface WriterListener extends EventListener{  
  4.   
  5.     void addNovel(WriterEvent writerEvent);  
  6.       
  7. }  

             這個監聽器猛地一看,特別像觀察者介面,它們承擔的功能是類似的,都是提供觀察者或者監聽者實現自己響應的行為規定,其中addNovel方法代表的是作者釋出新書時的響應。加入了這兩個類以後,我們原有的作者和讀者類就要發生點變化了,我們先來看作者類的變化。

[java] view plain copy
  1. import java.util.HashSet;  
  2. import java.util.Set;  
  3.   
  4. //作者類  
  5. public class Writer{  
  6.       
  7.     private String name;//作者的名稱  
  8.       
  9.     private String lastNovel;//記錄作者最新發布的小說  
  10.       
  11.     private Set<WriterListener> writerListenerList = new HashSet<WriterListener>();//作者類要包含一個自己監聽器的列表  
  12.   
  13.     public Writer(String name) {  
  14.         super();  
  15.         this.name = name;  
  16.         WriterManager.getInstance().add(this);  
  17.     }  
  18.   
  19.     //作者釋出新小說了,要通知所有關注自己的讀者  
  20.     public void addNovel(String novel) {  
  21.         System.out.println(name + "釋出了新書《" + novel + "》!");  
  22.         lastNovel = novel;  
  23.         fireEvent();  
  24.     }  
  25.     //觸發釋出新書的事件,通知所有監聽這件事的監聽器  
  26.     private void fireEvent(){  
  27.         WriterEvent writerEvent = new WriterEvent(this);  
  28.         for (WriterListener writerListener : writerListenerList) {  
  29.             writerListener.addNovel(writerEvent);  
  30.         }  
  31.     }  
  32.     //提供給外部註冊成為自己的監聽器的方法  
  33.     public void registerListener(WriterListener writerListener){  
  34.         writerListenerList.add(writerListener);  
  35.     }  
  36.     //提供給外部登出的方法  
  37.     public void unregisterListener(WriterListener writerListener){  
  38.         writerListenerList.remove(writerListener);  
  39.     }  
  40.       
  41.     public String getLastNovel() {  
  42.         return lastNovel;  
  43.     }  
  44.   
  45.     public String getName() {  
  46.         return name;  
  47.     }  
  48.   
  49. }  

                可以看到,作者類的主要變化是新增了一個自己的監聽器列表,我們使用set是為了它的天然去重效果,並且提供給外部註冊和登出的方法,與觀察者模式相比,這個功能本身是由基類Observable提供的,不過觀察者模式中有統一的觀察者Observer介面,但是監聽器沒有,雖說有EventListener這個超級介面,但它畢竟沒有任何行為。所以我們一般需要維持一個自己特有的監聽器列表。

                下面我們看讀者類的變化,如下。

[java] view plain copy
  1. public class Reader implements WriterListener{  
  2.   
  3.     private String name;  
  4.       
  5.     public Reader(String name) {  
  6.         super();  
  7.         this.name = name;  
  8.     }  
  9.   
  10.     public String getName() {  
  11.         return name;  
  12.     }  
  13.       
  14.     //讀者可以關注某一位作者,關注則代表把自己加到作者的監聽器列表裡  
  15.     public void subscribe(String writerName){  
  16.         WriterManager.getInstance().getWriter(writerName).registerListener(this);  
  17.     }  
  18.       
  19.     //讀者可以取消關注某一位作者,取消關注則代表把自己從作者的監聽器列表裡登出  
  20.     public void unsubscribe(String writerName){  
  21.         WriterManager.getInstance().getWriter(writerName).unregisterListener(this);  
  22.     }  
  23.       
  24.     public void addNovel(WriterEvent writerEvent) {  
  25.         Writer writer = writerEvent.getWriter();  
  26.         System.out.println(name+"知道" + writer.getName() + "釋出了新書《" + writer.getLastNovel() + "》,非要去看!");  
  27.     }  
  28.   
  29. }  

               讀者類的變化,首先本來是實現Observer介面,現在要實現WriterListener介面,響應的update方法就改為我們定義的addNovel方法,當中的響應基本沒變。另外就是關注和取消關注的方法中,原來是給作者類新增觀察者和刪除觀察者,現在是註冊監聽器和登出監聽器,幾乎是沒什麼變化的。

               我們徹底將剛才的觀察者模式改成了事件驅動,現在我們使用事件驅動的類再執行一下客戶端,其中客戶端程式碼和WriterManager類的程式碼是完全不需要改動的,直接執行客戶端即可。我們會發現得到的結果與觀察者模式一模一樣。

               走到這裡我們發現二者可以達到的效果一模一樣,那麼兩者是不是一樣呢?

               答案當然是否定的,首先我們從實現方式上就能看出,事件驅動可以解決觀察者模式的問題,但反過來則不一定,另外二者所表達的業務場景也不一樣,比如上述例子,使用觀察者模式更貼近業務場景的描述,而使用事件驅動,從業務上講,則有點勉強。

               二者除了業務場景的區別以外,在功能上主要有以下區別。

               1,觀察者模式中觀察者的響應理論上講針對特定的被觀察者是唯一的(說理論上唯一的原因是,如果你願意,你完全可以在update方法裡新增一系列的elseif去產生不同的響應,但LZ早就說過,你應該忘掉elseif),而事件驅動則不是,因為我們可以定義自己感興趣的事情,比如剛才,我們可以監聽作者釋出新書,我們還可以在監聽器介面中定義其它的行為。再比如tomcat中,我們可以監聽servletcontext的init動作,也可以監聽它的destroy動作。

               2,雖然事件驅動模型更加靈活,但也是付出了系統的複雜性作為代價的,因為我們要為每一個事件源定製一個監聽器以及事件,這會增加系統的負擔,各位看看tomcat中有多少個監聽器和事件類就知道了。

               3,另外觀察者模式要求被觀察者繼承Observable類,這就意味著如果被觀察者原來有父類的話,就需要自己實現被觀察者的功能,當然,這一尷尬事情,我們可以使用介面卡模式彌補,但也不可避免的造成了觀察者模式的侷限性。事件驅動中事件源則不需要,因為事件源所維護的監聽器列表是給自己定製的,所以無法去製作一個通用的父類去完成這個工作。

               4,被觀察者傳送給觀察者的資訊是模糊的,比如update中第二個引數,型別是Object,這需要觀察者和被觀察者之間有約定才可以使用這個引數。而在事件驅動模型中,這些資訊是被封裝在Event當中的,可以更清楚的告訴監聽器,每個資訊都是代表的什麼。

               由於上述使用事件驅動有點勉強,所以LZ給各位模擬一個我們js當中的一個事件驅動模型,就是按鈕的點選事件。

               在這個模型當中,按鈕自然就是事件源,而事件的種類有很多,比如點選(click),雙擊(dblclick),滑鼠移動事件(mousemove)。我們的監聽器與事件個數是一樣的,所以這也是事件驅動的弊端,我們需要一堆事件和監聽器,下面LZ一次性給出這三種事件和監聽器,其餘還有很多事件,類似,LZ這裡省略。

[java] view plain copy
  1. import java.util.EventObject;  
  2. //按鈕事件基類  
  3. public abstract class ButtonEvent extends EventObject{  
  4.   
  5.     public ButtonEvent(Object source) {  
  6.         super(source);  
  7.     }  
  8.   
  9.     public Button getButton(){  
  10.         return (Button) super.getSource();  
  11.     }  
  12. }  
  13. //點選事件  
  14. class ClickEvent extends ButtonEvent{  
  15.   
  16.     public ClickEvent(Object source) {  
  17.         super(source);  
  18.     }  
  19.   
  20. }  
  21. //雙擊事件  
  22. class DblClickEvent extends ButtonEvent{  
  23.   
  24.     public DblClickEvent(Object source) {  
  25.         super(source);  
  26.     }  
  27.   
  28. }  
  29. //滑鼠移動事件  
  30. class MouseMoveEvent extends ButtonEvent{  
  31.     //滑鼠移動事件比較特殊,因為它需要告訴監聽器滑鼠當前的座標是在哪,我們記錄為x,y  
  32.     private int x;  
  33.     private int y;  
  34.   
  35.     public MouseMoveEvent(Object source, int x, int y) {  
  36.         super(source);  
  37.         this.x = x;  
  38.         this.y = y;  
  39.     }  
  40.   
  41.     public int getX() {  
  42.         return x;  
  43.     }  
  44.   
  45.     public int getY() {  
  46.         return y;  
  47.     }  
  48.       
  49. }  

                     以上是三種事件,都非常簡單,只有滑鼠移動需要額外的座標,下面給出三種監聽器。

[java] view plain copy
  1. import java.util.EventListener;  
  2. //點選監聽器  
  3. interface ClickListener extends EventListener{  
  4.   
  5.     void click(ClickEvent clickEvent);  
  6.       
  7. }  
  8.   
  9. //雙擊監聽器  
  10. interface DblClickListener extends EventListener{  
  11.   
  12.     void dblClick(DblClickEvent dblClickEvent);  
  13.       
  14. }  
  15.   
  16. //滑鼠移動監聽器  
  17. interface MouseMoveListener extends EventListener{  
  18.   
  19.     void mouseMove(MouseMoveEvent mouseMoveEvent);  
  20.       
  21. }  

                    三種監聽器分別監聽點選,雙擊和滑鼠移動。下面給出我們最重要的類,Button。

[java] view plain copy
  1. //我們模擬一個html頁面的button元素,LZ只新增個別屬性,其餘屬性同理  
  2. public class Button {  
  3.       
  4.     private String id;//這相當於id屬性  
  5.     private String value;//這相當於value屬性  
  6.     private ClickListener onclick;//我們完全模擬原有的模型,這個其實相當於onclick屬性  
  7.     private DblClickListener onDblClick;//同理,這個相當於雙擊屬性  
  8.     private MouseMoveListener onMouseMove;//同理  
  9.       
  10.     //按鈕的單擊行為  
  11.     public void click(){  
  12.         onclick.click(new ClickEvent(this));  
  13.     }  
  14.     //按鈕的雙擊行為  
  15.     public void dblClick(){  
  16.         onDblClick.dblClick(new DblClickEvent(this));  
  17.     }  
  18.     //按鈕的滑鼠移動行為  
  19.     public void mouseMove(int x,int y){  
  20.         onMouseMove.mouseMove(new MouseMoveEvent(this,x,y));  
  21.     }  
  22.     //相當於給id賦值  
  23.     public void setId(String id) {  
  24.         this.id = id;  
  25.     }  
  26.     //類似  
  27.     public void setValue(String value) {  
  28.         this.value = value;  
  29.     }  
  30.     //這個相當於我們在給onclick新增函式,即設定onclick屬性  
  31.     public void setOnclick(ClickListener onclick) {  
  32.         this.onclick = onclick;  
  33.     }  
  34.     //同理  
  35.     public void setOnDblClick(DblClickListener onDblClick) {  
  36.         this.onDblClick = onDblClick;  
  37.     }  
  38.     //同理  
  39.     public void setOnMouseMove(MouseMoveListener onMouseMove) {  
  40.         this.onMouseMove = onMouseMove;  
  41.     }  
  42.     //以下get方法  
  43.     public String getId() {  
  44.         return id;  
  45.     }  
  46.       
  47.     public String getValue() {  
  48.         return value;  
  49.     }  
  50.       
  51.     public ClickListener getOnclick() {  
  52.         return onclick;  
  53.     }  
  54.       
  55.     public DblClickListener getOnDblClick() {  
  56.         return onDblClick;  
  57.     }  
  58.       
  59.     public MouseMoveListener getOnMouseMove() {  
  60.         return onMouseMove;  
  61.     }  
  62.       
  63. }  

                     可以看到,按鈕Button類有很多屬性,都是我們經常看到的,id,value,onclick等等。下面我們模擬編寫一個頁面,這個頁面可以當做是一個JSP頁面,我們只有一個按鈕,我們用JAVA語言把它描述出來,如下。

[java] view plain copy
  1. //假設這個是我們寫的某一個特定的jsp頁面,裡面可能有很多元素,input,form,table,等等  
  2. //我們假設只有一個按鈕  
  3. public class ButtonJsp {  
  4.   
  5.     private Button button;  
  6.   
  7.     public ButtonJsp() {  
  8.         super();  
  9.         button = new Button();//這個可以當做我們在頁面寫了一個button元素  
  10.         button.setId("submitButton");//取submitButton為id  
  11.         button.setValue("提交");//提交按鈕  
  12.         button.setOnclick(new ClickListener() {//我們給按鈕註冊點選監聽器  
  13.             //按鈕被點,我們就驗證後提交  
  14.             public void click(ClickEvent clickEvent) {  
  15.                 System.out.println("--------單擊事件程式碼---------");  
  16.                 System.out.println("if('表單合法'){");  
  17.                 System.out.println("\t表單提交");  
  18.                 System.out.println("}else{");  
  19.                 System.out.println("\treturn false");  
  20.                 System.out.println("}");  
  21.             }  
  22.         });  
  23.         button.setOnDblClick(new DblClickListener() {  
  24.             //雙擊的話我們提示使用者不能雙擊“提交”按鈕  
  25.             public void dblClick(DblClickEvent dblClickEvent) {  
  26.                 System.out.println("--------雙擊事件程式碼---------");  
  27.                 System.out.println("alert('您不能雙擊"+dblClickEvent.getButton().getValue()+"按鈕')");  
  28.             }  
  29.         });  
  30.         button.setOnMouseMove(new MouseMoveListener() {  
  31.             //這個我們只簡單提示使用者滑鼠當前位置,示例中加入這個事件  
  32.             //目的只是為了說明事件驅動中,可以包含一些特有的資訊,比如座標  
  33.             public void mouseMove(MouseMoveEvent mouseMoveEvent) {  
  34.                 System.out.println("--------滑鼠移動程式碼---------");  
  35.                 System.out.println("alert('您當前滑鼠的位置,x座標為:"+mouseMoveEvent.getX()+",y座標為:"+mouseMoveEvent.getY()+"')");  
  36.             }  
  37.         });  
  38.     }  
  39.   
  40.     public Button getButton() {  
  41.         return button;  
  42.     }  
  43.       
  44. }  

                  以上可以認為我們給web服務中寫了一個簡單的頁面,下面我們看客戶在訪問我們的頁面時,我們的頁面在做什麼。

[java] view plain copy
  1. public class Client {  
  2.   
  3.     public static void main(String[] args) {  
  4.         ButtonJsp jsp = new ButtonJsp();//客戶訪問了我們的這個JSP頁面  
  5.         //以下客戶開始在按鈕上操作  
  6.         jsp.getButton().dblClick();//雙擊按鈕  
  7.         jsp.getButton().mouseMove(10100);//移動到10,100  
  8.         jsp.getButton().mouseMove(1590);//又移動到15,90  
  9.         jsp.getButton().click();//接著客戶點了提交  
  10.     }  
  11. }  

                我們看執行結果可以看到,我們的三個事件都起了作用,最終提交了表單。



                以上就是模擬整個JSP頁面中,我們的按鈕響應使用者事件的過程,我相信通過這兩個例子,各位應該對觀察者模式和事件驅動都有了自己的理解和認識,二者都是用來處理變化與響應的問題,其中觀察者更多的是釋出-訂閱,也就是類似讀者和作者的關係,而事件驅動更多的是為了響應客戶的請求,從而制定一系列的事件和監聽器,去處理客戶的請求與操作。

               二者其實都是有自己的弱項的,只有掌握了模式的弱項才能更好的使用,不是有句話叫“真正瞭解一個東西,不是知道它能幹什麼,而是知道它不能幹什麼。”嗎?

               觀察者模式所欠缺的是設計上的問題,即觀察者和被觀察者是多對一的關係,那麼反過來的話,就無法支援了。

               各位可以嘗試將二者位置互換達到這個效果,這算是設計模式的活用,很簡單,就是讓被觀察者做成一個介面,提供是否改變的方法,讓觀察者維護一個被觀察者的列表,另外開啟一個執行緒去不斷的測試各個被觀察者是否改變。由於本篇已經夠長,所以LZ不再詳細編寫,如果有哪位讀者有需要,可以在下方留言,LZ看到的話,如果有時間,會寫出來放到資源裡供各位下載。

               觀察者模式還有一個缺點就是,每一個觀察者都要實現觀察者介面,才能新增到被觀察者的列表當中,假設一個觀察者已經存在,而且我們無法改變其程式碼,那麼就無法讓它成為一個觀察者了,不過這個我們依然可以使用介面卡模式解決。但是還有一個問題就不好解決了,就是假如我們很多類都是現成的,當被觀察者發生變化時,每一個觀察者都需要呼叫不同的方法,那麼觀察者模式就有點捉襟見肘的感覺了,我們必須適配每一個類去統一他們變化的方法名稱為update,這是一個很可怕的事情。

               對於事件驅動就沒有這樣的問題,我們可以實現多個監聽器來達到監聽多個事件源的目的,但是它的缺點剛才已經說過了,在事件源或者事件增加時,監聽器和事件類通常情況下會成對增加,造成系統的複雜性增加,不過目前看來,事件驅動模型一般都比較穩定,所以這個問題並不太明顯,因為很少見到無限增加事件的情況發生。

               還有一個缺點就是我們的事件源需要看準時機觸發自己的各個監聽器,這也從某種意義上增加了事件源的負擔,造成了類一定程度上的臃腫。

               最後,LZ再總結下二者針對的業務場景概述。

               觀察者模式:釋出(release)--訂閱(subscibe),變化(change)--更新(update)

               事件驅動模型:請求(request)--響應(response),事件發生(occur)--事件處理(handle)       
               感謝各位的收看。

相關文章