(六)觀察者模式詳解(包含觀察者模式JDK的漏洞以及事件驅動模型)決了當時的問題,那時LZ接觸JAVA剛幾個月,比葫蘆畫瓢的用了觀察者模式。

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

                 提及觀察者模式,LZ仍是很是激動的,當初這算是第一個讓LZ感覺到設計模式強大的傢伙。當初LZ要作一個小型WEB項目,要上傳給服務器文件,一個需求就是要顯示上傳進度,LZ就是用這個模式解決了當時的問題,那時LZ接觸JAVA剛幾個月,比葫蘆畫瓢的用了觀察者模式。java

                 如今談及觀察者模式,能用到的地方就相對較多了,一般意義上若是一個對象狀態的改變須要通知不少對這個對象關注的一系列對象,就可使用觀察者模式。web

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

                 定義:觀察者模式(有時又被稱爲發佈-訂閱模式、模型-視圖模式、源-收聽者模式或從屬者模式)是軟件設計模式的一種。在此種模式中,一個目標物件管理全部相依於它的觀察者物件,而且在它自己的狀態改變時主動發出通知。這一般透過呼叫各觀察者所提供的方法來實現。此種模式一般被用來實做事件處理系統。編程

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

                 簡單點歸納成通俗的話來講,就是一個類管理着全部依賴於它的觀察者類,而且它狀態變化時會主動給這些依賴它的類發出通知。數組

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


                 能夠看到,咱們的被觀察者類Observable只關聯了一個Observer的列表,而後在本身狀態變化時,使用notifyObservers方法通知這些Observer,具體這些Observer都是什麼,被觀察者是不關心也不須要知道的。 服務器

                上面就將觀察者和被觀察者兩者的耦合度降到很低了,而咱們具體的觀察者是必需要知道本身觀察的是誰,因此它依賴於被觀察者。 併發

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

                首先是觀察者接口。

複製代碼
package net; //這個接口是爲了提供一個統一的觀察者作出相應行爲的方法 public interface Observer { void update(Observable o); }
複製代碼

                再者是具體的觀察者。

複製代碼
package net; public class ConcreteObserver1 implements Observer{ public void update(Observable o) { System.out.println("觀察者1觀察到" + o.getClass().getSimpleName() + "發生變化"); System.out.println("觀察者1作出相應"); } }
複製代碼
複製代碼
package net; public class ConcreteObserver2 implements Observer{ public void update(Observable o) { System.out.println("觀察者2觀察到" + o.getClass().getSimpleName() + "發生變化"); System.out.println("觀察者2作出相應"); } }
複製代碼

                下面是被觀察者,它有一個觀察者的列表,而且有一個通知全部觀察者的方法,通知的方式就是調用觀察者通用的接口行爲update方法。下面咱們看它的代碼。

複製代碼
package net; import java.util.ArrayList; import java.util.List; public class Observable { List<Observer> observers = new ArrayList<Observer>(); public void addObserver(Observer o){ observers.add(o); } public void changed(){ System.out.println("我是被觀察者,我已經發生變化了"); notifyObservers();//通知觀察本身的全部觀察者  } public void notifyObservers(){ for (Observer observer : observers) { observer.update(this); } } }
複製代碼

                這裏面很簡單,新增兩個方法,一個是爲了改變本身的同時通知觀察者們,一個是爲了給客戶端一個添加觀察者的公共接口。

                下面咱們使用客戶端調用一下,看一下客戶端如何操做。

複製代碼
package net; public class Client { public static void main(String[] args) throws Exception { Observable observable = new Observable(); observable.addObserver(new ConcreteObserver1()); observable.addObserver(new ConcreteObserver2()); observable.changed(); } }
複製代碼

                 運行結果以下。

 

                 能夠看到咱們在操做被觀察者時,只要調用changed方法,觀察者們就會作出相應的動做,而添加觀察者這個行爲算是準備階段,將具體的觀察者關聯到被觀察者上面去。 

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

                 因爲JDK中爲了方便開發人員,已經寫好了現成的觀察者接口和被觀察者類,下面LZ先給出JDK中現成的觀察者和被觀察者代碼,外加本身的一點解釋,來幫助一些讀者對JDK中對觀察者模式的支持熟悉一下。

                 先來觀察者接口。

//觀察者接口,每個觀察者都必須實現這個接口 public interface Observer { //這個方法是觀察者在觀察對象產生變化時所作的響應動做,從中傳入了觀察的對象和一個預留參數 void update(Observable o, Object arg); }

                下面是被觀察者類。

複製代碼
import java.util.Vector; //被觀察者類 public class Observable { //這是一個改變標識,來標記該被觀察者有沒有改變 private boolean changed = false; //持有一個觀察者列表 private Vector obs; public Observable() { obs = new Vector(); } //添加觀察者,添加時會去重 public synchronized void addObserver(Observer o) { if (o == null) throw new NullPointerException(); if (!obs.contains(o)) { obs.addElement(o); } } //刪除觀察者 public synchronized void deleteObserver(Observer o) { obs.removeElement(o); } //notifyObservers(Object arg)的重載方法 public void notifyObservers() { notifyObservers(null); } //通知全部觀察者,被觀察者改變了,你能夠執行你的update方法了。 public void notifyObservers(Object arg) { //一個臨時的數組,用於併發訪問被觀察者時,留住觀察者列表的當前狀態,這種處理方式其實也算是一種設計模式,即備忘錄模式。  Object[] arrLocal; //注意這個同步塊,它表示在獲取觀察者列表時,該對象是被鎖定的 //也就是說,在我獲取到觀察者列表以前,不容許其餘線程改變觀察者列表 synchronized (this) { //若是沒變化直接返回 if (!changed) return; //這裏將當前的觀察者列表放入臨時數組 arrLocal = obs.toArray(); //將改變標識從新置回未改變  clearChanged(); } //注意這個for循環沒有在同步塊,此時已經釋放了被觀察者的鎖,其餘線程能夠改變觀察者列表 //可是這並不影響咱們當前進行的操做,由於咱們已經將觀察者列表複製到臨時數組 //在通知時咱們只通知數組中的觀察者,當前刪除和添加觀察者,都不會影響咱們通知的對象 for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } //刪除全部觀察者 public synchronized void deleteObservers() { obs.removeAllElements(); } //標識被觀察者被改變過了 protected synchronized void setChanged() { changed = true; } //標識被觀察者沒改變 protected synchronized void clearChanged() { changed = false; } //返回被觀察者是否改變 public synchronized boolean hasChanged() { return changed; } //返回觀察者數量 public synchronized int countObservers() { return obs.size(); } }
複製代碼

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

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

                 在循環遍歷觀察者讓觀察者作出響應時,JDK沒有去抓取update方法中的異常,因此假設在這過程當中有一個update方法拋出了異常,那麼剩下還未通知的觀察者就全都通知不到了,因此LZ我的比較疑惑這樣的用意(LZ沒法想象JAVA類庫的製造者沒考慮到這個問題),是sun當時真的忘了考慮這一點,仍是另有它意?固然各位讀者若是有本身的看法能夠告知LZ,不過LZ認爲,無論是sun如此作是別有用意,仍是真的欠考慮,咱們都要注意在update方法裏必定要處理好異常,我的以爲JDK中比較保險的作法仍是以下這樣。

for (int i = arrLocal.length-1; i>=0; i--){ try { ((Observer)arrLocal[i]).update(this, arg); } catch (Throwable e) {e.printStackTrace();} }

                 這樣不管其中任何一個update是否成功都不會影響其他的觀察者進行更新狀態,咱們本身比較保險的作法就是給update方法整個加上try塊,或者確認不會發生運行時異常。

 

                 上面LZ和各位一塊兒分析了JDK中觀察者模式的源碼,下面咱們就拿上述小說網的例子,作一個DEMO。

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

複製代碼
//讀者類,要實現觀察者接口 public class Reader implements Observer{ private String name; public Reader(String name) { super(); this.name = name; } public String getName() { return name; } //讀者能夠關注某一位做者,關注則表明把本身加到做者的觀察者列表裏 public void subscribe(String writerName){ WriterManager.getInstance().getWriter(writerName).addObserver(this); } //讀者能夠取消關注某一位做者,取消關注則表明把本身從做者的觀察者列表裏刪除 public void unsubscribe(String writerName){ WriterManager.getInstance().getWriter(writerName).deleteObserver(this); } //當關注的做者發表新小說時,會通知讀者去看 public void update(Observable o, Object obj) { if (o instanceof Writer) { Writer writer = (Writer) o; System.out.println(name+"知道" + writer.getName() + "發佈了新書《" + writer.getLastNovel() + "》,非要去看!"); } } }
複製代碼

                       下面是做者類。

複製代碼
//做者類,要繼承自被觀察者類 public class Writer extends Observable{ private String name;//做者的名稱 private String lastNovel;//記錄做者最新發布的小說 public Writer(String name) { super(); this.name = name; WriterManager.getInstance().add(this); } //做者發佈新小說了,要通知全部關注本身的讀者 public void addNovel(String novel) { System.out.println(name + "發佈了新書《" + novel + "》!"); lastNovel = novel; setChanged(); notifyObservers(); } public String getLastNovel() { return lastNovel; } public String getName() { return name; } }
複製代碼

                 而後咱們還須要一個管理器幫咱們管理這些做者。以下。

複製代碼
import java.util.HashMap; import java.util.Map; //管理器,保持一份獨有的做者列表 public class WriterManager{ private Map<String, Writer> writerMap = new HashMap<String, Writer>(); //添加做者 public void add(Writer writer){ writerMap.put(writer.getName(), writer); } //根據做者姓名獲取做者 public Writer getWriter(String name){ return writerMap.get(name); } //單例 private WriterManager(){} public static WriterManager getInstance(){ return WriterManagerInstance.instance; } private static class WriterManagerInstance{ private static WriterManager instance = new WriterManager(); } }
複製代碼

                好了,這下咱們的觀察者模式就作好了,這個簡單的DEMO能夠支持讀者關注做者,看成者發佈新書時,讀者會觀察到這個事情,會產生相應的動做。下面咱們寫個客戶端調用一下。

複製代碼
//客戶端調用 public class Client { public static void main(String[] args) { //假設四個讀者,兩個做者 Reader r1 = new Reader("謝廣坤"); Reader r2 = new Reader("趙四"); Reader r3 = new Reader("七哥"); Reader r4 = new Reader("劉能"); Writer w1 = new Writer("謝大腳"); Writer w2 = new Writer("王小蒙"); //四人關注了謝大腳 r1.subscribe("謝大腳"); r2.subscribe("謝大腳"); r3.subscribe("謝大腳"); r4.subscribe("謝大腳"); //七哥和劉能還關注了王小蒙 r3.subscribe("王小蒙"); r4.subscribe("王小蒙"); //做者發佈新書就會通知關注的讀者 //謝大腳寫了設計模式 w1.addNovel("設計模式"); //王小蒙寫了JAVA編程思想 w2.addNovel("JAVA編程思想"); //謝廣坤取消關注謝大腳 r1.unsubscribe("謝大腳"); //謝大腳再寫書將不會通知謝廣坤 w1.addNovel("觀察者模式"); } }
複製代碼

                    看下咱們獲得的結果,就會發現,咱們確實通知了讀者它所關注的做者的動態,並且讀者取消關注之後,做者的動態將再也不通知該讀者。下面是運行結果。

                咱們使用觀察者模式的用意是爲了做者再也不須要關心他發佈新書時都要去通知誰,更重要的是他不須要關心他通知的是讀者仍是其它什麼人,他只知道這我的是實現了觀察者接口的,即咱們的被觀察者依賴的只是一個抽象的接口觀察者接口,而不關心具體的觀察者都有誰都是什麼,好比之後要是遊客也能夠關注做者了,那麼只要遊客類實現觀察者接口,那麼同樣能夠將遊客列入到做者的觀察者列表中。

                另外,咱們讓讀者本身來選擇本身關注的對象,這至關於被觀察者將維護通知對象的職能轉化給了觀察者,這樣作的好處是因爲一個被觀察者可能有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當中依然有現成的一套事件模型類庫,其中監聽器只是一個標識接口,由於它沒有表達對具體對象感興趣的意思,因此也沒法定義監聽的事件,只是爲了統一,用來給特定的監聽器繼承。它的源代碼以下。

複製代碼
package java.util; /** * A tagging interface that all event listener interfaces must extend. * @since JDK1.1 */ public interface EventListener { }
複製代碼

                因爲代碼很短,因此LZ沒有刪減,當中標註了,全部的事件監聽器都必須繼承,這是一個標識接口。上述的事件,JDK當中也有一個現成的類供繼承,就是EventObject,這個類的源代碼以下。

複製代碼
public class EventObject implements java.io.Serializable { private static final long serialVersionUID = 5516075349620653480L; /** * The object on which the Event initially occurred. */ protected transient Object source; /** * Constructs a prototypical Event. * * @param source The object on which the Event initially occurred. * @exception IllegalArgumentException if source is null. */ public EventObject(Object source) { if (source == null) throw new IllegalArgumentException("null source"); this.source = source; } /** * The object on which the Event initially occurred. * * @return The object on which the Event initially occurred. */ public Object getSource() { return source; } /** * Returns a String representation of this EventObject. * * @return A a String representation of this EventObject. */ public String toString() { return getClass().getName() + "[source=" + source + "]"; } }
複製代碼

             這個類並不複雜,它只是想代表,全部的事件都應該帶有一個事件源,大部分狀況下,這個事件源就是咱們被監聽的對象。

             若是咱們採用事件驅動模型去分析上面的例子,那麼做者就是事件源,而讀者就是監聽器,依據這個思想,咱們把上述例子改一下,首先咱們須要自定義咱們本身的監聽器和事件。因此咱們定義以下做者事件。

複製代碼
import java.util.EventObject; public class WriterEvent extends EventObject{ private static final long serialVersionUID = 8546459078247503692L; public WriterEvent(Writer writer) { super(writer); } public Writer getWriter(){ return (Writer) super.getSource(); } }
複製代碼

              這表明了一個做者事件,這個事件當中通常就是包含一個事件源,在這裏就是做者,固然有的時候你可讓它帶有更多的信息,以方便監聽器作出更加細緻的動做。下面咱們定義以下監聽器。

複製代碼
import java.util.EventListener; public interface WriterListener extends EventListener{ void addNovel(WriterEvent writerEvent); }
複製代碼

             這個監聽器猛地一看,特別像觀察者接口,它們承擔的功能是相似的,都是提供觀察者或者監聽者實現本身響應的行爲規定,其中addNovel方法表明的是做者發佈新書時的響應。加入了這兩個類之後,咱們原有的做者和讀者類就要發生點變化了,咱們先來看做者類的變化。

複製代碼
import java.util.HashSet; import java.util.Set; //做者類 public class Writer{ private String name;//做者的名稱 private String lastNovel;//記錄做者最新發布的小說 private Set<WriterListener> writerListenerList = new HashSet<WriterListener>();//做者類要包含一個本身監聽器的列表 public Writer(String name) { super(); this.name = name; WriterManager.getInstance().add(this); } //做者發佈新小說了,要通知全部關注本身的讀者 public void addNovel(String novel) { System.out.println(name + "發佈了新書《" + novel + "》!"); lastNovel = novel; fireEvent(); } //觸發發佈新書的事件,通知全部監聽這件事的監聽器 private void fireEvent(){ WriterEvent writerEvent = new WriterEvent(this); for (WriterListener writerListener : writerListenerList) { writerListener.addNovel(writerEvent); } } //提供給外部註冊成爲本身的監聽器的方法 public void registerListener(WriterListener writerListener){ writerListenerList.add(writerListener); } //提供給外部註銷的方法 public void unregisterListener(WriterListener writerListener){ writerListenerList.remove(writerListener); } public String getLastNovel() { return lastNovel; } public String getName() { return name; } }
複製代碼

                能夠看到,做者類的主要變化是添加了一個本身的監聽器列表,咱們使用set是爲了它的自然去重效果,而且提供給外部註冊和註銷的方法,與觀察者模式相比,這個功能自己是由基類Observable提供的,不過觀察者模式中有統一的觀察者Observer接口,可是監聽器沒有,雖然說有EventListener這個超級接口,但它畢竟沒有任何行爲。因此咱們通常須要維持一個本身特有的監聽器列表。

                下面咱們看讀者類的變化,以下。

複製代碼
public class Reader implements WriterListener{ private String name; public Reader(String name) { super(); this.name = name; } public String getName() { return name; } //讀者能夠關注某一位做者,關注則表明把本身加到做者的監聽器列表裏 public void subscribe(String writerName){ WriterManager.getInstance().getWriter(writerName).registerListener(this); } //讀者能夠取消關注某一位做者,取消關注則表明把本身從做者的監聽器列表裏註銷 public void unsubscribe(String writerName){ WriterManager.getInstance().getWriter(writerName).unregisterListener(this); } public void addNovel(WriterEvent writerEvent) { Writer writer = writerEvent.getWriter(); System.out.println(name+"知道" + writer.getName() + "發佈了新書《" + writer.getLastNovel() + "》,非要去看!"); } }
複製代碼

               讀者類的變化,首先原本是實現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這裏省略。

複製代碼
import java.util.EventObject; //按鈕事件基類 public abstract class ButtonEvent extends EventObject{ public ButtonEvent(Object source) { super(source); } public Button getButton(){ return (Button) super.getSource(); } } //點擊事件 class ClickEvent extends ButtonEvent{ public ClickEvent(Object source) { super(source); } } //雙擊事件 class DblClickEvent extends ButtonEvent{ public DblClickEvent(Object source) { super(source); } } //鼠標移動事件 class MouseMoveEvent extends ButtonEvent{ //鼠標移動事件比較特殊,由於它須要告訴監聽器鼠標當前的座標是在哪,咱們記錄爲x,y private int x; private int y; public MouseMoveEvent(Object source, int x, int y) { super(source); this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } }
複製代碼

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

複製代碼
import java.util.EventListener; //點擊監聽器 interface ClickListener extends EventListener{ void click(ClickEvent clickEvent); } //雙擊監聽器 interface DblClickListener extends EventListener{ void dblClick(DblClickEvent dblClickEvent); } //鼠標移動監聽器 interface MouseMoveListener extends EventListener{ void mouseMove(MouseMoveEvent mouseMoveEvent); }
複製代碼

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

複製代碼
//咱們模擬一個html頁面的button元素,LZ只添加個別屬性,其他屬性同理 public class Button { private String id;//這至關於id屬性 private String value;//這至關於value屬性 private ClickListener onclick;//咱們徹底模擬原有的模型,這個其實至關於onclick屬性 private DblClickListener onDblClick;//同理,這個至關於雙擊屬性 private MouseMoveListener onMouseMove;//同理 //按鈕的單擊行爲 public void click(){ onclick.click(new ClickEvent(this)); } //按鈕的雙擊行爲 public void dblClick(){ onDblClick.dblClick(new DblClickEvent(this)); } //按鈕的鼠標移動行爲 public void mouseMove(int x,int y){ onMouseMove.mouseMove(new MouseMoveEvent(this,x,y)); } //至關於給id賦值 public void setId(String id) { this.id = id; } //相似 public void setValue(String value) { this.value = value; } //這個至關於咱們在給onclick添加函數,即設置onclick屬性 public void setOnclick(ClickListener onclick) { this.onclick = onclick; } //同理 public void setOnDblClick(DblClickListener onDblClick) { this.onDblClick = onDblClick; } //同理 public void setOnMouseMove(MouseMoveListener onMouseMove) { this.onMouseMove = onMouseMove; } //如下get方法 public String getId() { return id; } public String getValue() { return value; } public ClickListener getOnclick() { return onclick; } public DblClickListener getOnDblClick() { return onDblClick; } public MouseMoveListener getOnMouseMove() { return onMouseMove; } }
複製代碼

                     能夠看到,按鈕Button類有不少屬性,都是咱們常常看到的,id,value,onclick等等。下面咱們模擬編寫一個頁面,這個頁面能夠當作是一個JSP頁面,咱們只有一個按鈕,咱們用JAVA語言把它描述出來,以下。

複製代碼
//假設這個是咱們寫的某一個特定的jsp頁面,裏面可能有不少元素,input,form,table,等等 //咱們假設只有一個按鈕 public class ButtonJsp { private Button button; public ButtonJsp() { super(); button = new Button();//這個能夠當作咱們在頁面寫了一個button元素 button.setId("submitButton");//取submitButton爲id button.setValue("提交");//提交按鈕 button.setOnclick(new ClickListener() {//咱們給按鈕註冊點擊監聽器 //按鈕被點,咱們就驗證後提交 public void click(ClickEvent clickEvent) { System.out.println("--------單擊事件代碼---------"); System.out.println("if('表單合法'){"); System.out.println("\t表單提交"); System.out.println("}else{"); System.out.println("\treturn false"); System.out.println("}"); } }); button.setOnDblClick(new DblClickListener() { //雙擊的話咱們提示用戶不能雙擊「提交」按鈕 public void dblClick(DblClickEvent dblClickEvent) { System.out.println("--------雙擊事件代碼---------"); System.out.println("alert('您不能雙擊"+dblClickEvent.getButton().getValue()+"按鈕')"); } }); button.setOnMouseMove(new MouseMoveListener() { //這個咱們只簡單提示用戶鼠標當前位置,示例中加入這個事件 //目的只是爲了說明事件驅動中,能夠包含一些特有的信息,好比座標 public void mouseMove(MouseMoveEvent mouseMoveEvent) { System.out.println("--------鼠標移動代碼---------"); System.out.println("alert('您當前鼠標的位置,x座標爲:"+mouseMoveEvent.getX()+",y座標爲:"+mouseMoveEvent.getY()+"')"); } }); } public Button getButton() { return button; } }
複製代碼

                  以上能夠認爲咱們給web服務中寫了一個簡單的頁面,下面咱們看客戶在訪問咱們的頁面時,咱們的頁面在作什麼。

複製代碼
public class Client { public static void main(String[] args) { ButtonJsp jsp = new ButtonJsp();//客戶訪問了咱們的這個JSP頁面 //如下客戶開始在按鈕上操做 jsp.getButton().dblClick();//雙擊按鈕 jsp.getButton().mouseMove(10, 100);//移動到10,100 jsp.getButton().mouseMove(15, 90);//又移動到15,90 jsp.getButton().click();//接着客戶點了提交  } }
複製代碼

                咱們看運行結果能夠看到,咱們的三個事件都起了做用,最終提交了表單。



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

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

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

               各位能夠嘗試將兩者位置互換達到這個效果,這算是設計模式的活用,很簡單,就是讓被觀察者作成一個接口,提供是否改變的方法,讓觀察者維護一個被觀察者的列表,另外開啓一個線程去不斷的測試各個被觀察者是否改變。因爲本篇已經夠長,因此LZ再也不詳細編寫,若是有哪位讀者有須要,能夠在下方留言,LZ看到的話,若是有時間,會寫出來放到資源裏供各位下載。

               觀察者模式還有一個缺點就是,每個觀察者都要實現觀察者接口,才能添加到被觀察者的列表當中,假設一個觀察者已經存在,並且咱們沒法改變其代碼,那麼就沒法讓它成爲一個觀察者了,不過這個咱們依然可使用適配器模式解決。可是還有一個問題就很差解決了,就是假如咱們不少類都是現成的,當被觀察者發生變化時,每個觀察者都須要調用不一樣的方法,那麼觀察者模式就有點捉襟見肘的感受了,咱們必須適配每個類去統一他們變化的方法名稱爲update,這是一個很可怕的事情。

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

               還有一個缺點就是咱們的事件源須要看準時機觸發本身的各個監聽器,這也從某種意義上增長了事件源的負擔,形成了類必定程度上的臃腫。

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

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

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

               下期預告,策略模式。

相關文章
相關標籤/搜索