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


                 作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。

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

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

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

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

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

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

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

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


                 可以看到,我們的被觀察者類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)       
               感謝各位的收看。

               下期預告,策略模式。

                  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM