(六)观察者模式详解(包含观察者模式JDK的漏洞以及事件驱动模型)

1年前 (2023) 程序员胖胖胖虎阿
130 0 0

                 作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可。

                 本章我们讨论一个除前面的单例以及代理模式之外,一个WEB项目中有可能用到的设计模式,即观察者模式。

                 说起观察者模式,LZ还是非常激动的,当初这算是第一个让LZ感受到设计模式强大的家伙。当初LZ要做一个小型WEB项目,要上传给服务器文件,一个需求就是要显示上传进度,LZ就是用这个模式解决了当时的问题,那时LZ接触JAVA刚几个月,比葫芦画瓢的用了观察者模式。

                 现在谈及观察者模式,能用到的地方就相对较多了,通常意义上如果一个对象状态的改变需要通知很多对这个对象关注的一系列对象,就可以使用观察者模式。

                 下面LZ先给出观察者模式标准版的定义,引自百度百科。

                 定义:观察者模式(有时又被称为发布-订阅模式、模型-视图模式、源-收听者模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实作事件处理系统。

                 上面的定义当中,主要有这样几个意思,首先是有一个目标的物件,通俗点讲就是一个类,它管理了所有依赖于它的观察者物件,或者通俗点说是观察者类,并在它自己状态发生变化时,主动发出通知。

                 简单点概括成通俗的话来说,就是一个类管理着所有依赖于它的观察者类,并且它状态变化时会主动给这些依赖它的类发出通知。

                 那么我们针对上面的描述给出观察者模式的类图,百度百科没有给出观察者模式的类图,这里LZ自己使用工具给各位画一个。

(六)观察者模式详解(包含观察者模式JDK的漏洞以及事件驱动模型)


                 可以看到,我们的被观察者类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();
    }
}

                 运行结果如下。

(六)观察者模式详解(包含观察者模式JDK的漏洞以及事件驱动模型)

 


                 可以看到我们在操作被观察者时,只要调用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("观察者模式");
    }
    
}

                    看下我们得到的结果,就会发现,我们确实通知了读者它所关注的作者的动态,而且读者取消关注以后,作者的动态将不再通知该读者。下面是运行结果。

(六)观察者模式详解(包含观察者模式JDK的漏洞以及事件驱动模型)

                我们使用观察者模式的用意是为了作者不再需要关心他发布新书时都要去通知谁,更重要的是他不需要关心他通知的是读者还是其它什么人,他只知道这个人是实现了观察者接口的,即我们的被观察者依赖的只是一个抽象的接口观察者接口,而不关心具体的观察者都有谁都是什么,比如以后要是游客也可以关注作者了,那么只要游客类实现观察者接口,那么一样可以将游客列入到作者的观察者列表中。

                另外,我们让读者自己来选择自己关注的对象,这相当于被观察者将维护通知对象的职能转化给了观察者,这样做的好处是由于一个被观察者可能有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();//接着客户点了提交
    }
}

                我们看运行结果可以看到,我们的三个事件都起了作用,最终提交了表单。

(六)观察者模式详解(包含观察者模式JDK的漏洞以及事件驱动模型)


                以上就是模拟整个JSP页面中,我们的按钮响应用户事件的过程,我相信通过这两个例子,各位应该对观察者模式和事件驱动都有了自己的理解和认识,二者都是用来处理变化与响应的问题,其中观察者更多的是发布-订阅,也就是类似读者和作者的关系,而事件驱动更多的是为了响应客户的请求,从而制定一系列的事件和监听器,去处理客户的请求与操作。


               二者其实都是有自己的弱项的,只有掌握了模式的弱项才能更好的使用,不是有句话叫“真正了解一个东西,不是知道它能干什么,而是知道它不能干什么。”吗?


               观察者模式所欠缺的是设计上的问题,即观察者和被观察者是多对一的关系,那么反过来的话,就无法支持了。


               各位可以尝试将二者位置互换达到这个效果,这算是设计模式的活用,很简单,就是让被观察者做成一个接口,提供是否改变的方法,让观察者维护一个被观察者的列表,另外开启一个线程去不断的测试各个被观察者是否改变。由于本篇已经够长,所以LZ不再详细编写,如果有哪位读者有需要,可以在下方留言,LZ看到的话,如果有时间,会写出来放到资源里供各位下载。


               观察者模式还有一个缺点就是,每一个观察者都要实现观察者接口,才能添加到被观察者的列表当中,假设一个观察者已经存在,而且我们无法改变其代码,那么就无法让它成为一个观察者了,不过这个我们依然可以使用适配器模式解决。但是还有一个问题就不好解决了,就是假如我们很多类都是现成的,当被观察者发生变化时,每一个观察者都需要调用不同的方法,那么观察者模式就有点捉襟见肘的感觉了,我们必须适配每一个类去统一他们变化的方法名称为update,这是一个很可怕的事情。


               对于事件驱动就没有这样的问题,我们可以实现多个监听器来达到监听多个事件源的目的,但是它的缺点刚才已经说过了,在事件源或者事件增加时,监听器和事件类通常情况下会成对增加,造成系统的复杂性增加,不过目前看来,事件驱动模型一般都比较稳定,所以这个问题并不太明显,因为很少见到无限增加事件的情况发生。


               还有一个缺点就是我们的事件源需要看准时机触发自己的各个监听器,这也从某种意义上增加了事件源的负担,造成了类一定程度上的臃肿。


               最后,LZ再总结下二者针对的业务场景概述。


               观察者模式:发布(release)--订阅(subscibe),变化(change)--更新(update)


               事件驱动模型:请求(request)--响应(response),事件发生(occur)--事件处理(handle)       


               感谢各位的收看。


               下期预告,策略模式。


                  

 

相关文章

暂无评论

暂无评论...