为了账号安全,请及时绑定邮箱和手机立即绑定

事件驱动模型与观察者模式

标签:
Java 设计

观察者更多的强调的是发布-订阅式的问题处理,而事件驱动则更多的注重于界面与数据模型之间的问题。
tomcat是一个app服务器,在使用的过程中,或许经常会有人用到listener,即监听器这个概念。那么其实这个就是一个事件驱动模型的应用。比如我们的spring,我们在应用启动的时候要初始化我们的IOC容器,那么我们的做法就是加入一个listener,这样伴随着tomcat服务器的启动,spring的IOC容器就会跟着启动。
那么这个listener其实就是事件驱动模型中的监听器,它用来监听它所感兴趣的事,比如我们springIOC容器启动的监听器,就是实现的ServletContextListener这个接口,说明它对servletContext感兴趣,会监听servletContext的启动和销毁。

JDK当中依然有现成的一套事件模型类库,其中监听器只是一个标识接口,因为它没有表达对具体对象感兴趣的意思,所以也无法定义监听的事件,只是为了统一,用来给特定的监听器继承。它的源代码如下。
其中标注了,所有的事件监听器都必须继承,这是一个标识接口。

package java.util;

/**
 * A tagging interface that all event listener interfaces must extend.
 * @since JDK1.1
 */
public interface EventListener {
}

事件,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接口,响应的update方法;事件驱动模式中实现的是WriterListener接口,响应的方法为我们定义的addNovel方法,当中的响应具体动作基本没变。
另外就是关注和取消关注的方法中,原来是给作者类添加观察者和删除观察者,现在是注册监听器和注销监听器,几乎是没什么变化的。
测试类(还是之前的观察中的测试类):

//客户端调用
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("观察者模式");
    }

}

运行结果和观察者模式中一毛一样。
从实现方式上就能看出,事件驱动可以解决观察者模式的问题,但反过来则不一定,另外二者所表达的业务场景也不一样,使用观察者模式更贴近业务场景的描述,而使用事件驱动,从业务上讲,则有点勉强。
二者除了业务场景的区别以外,在功能上主要有以下区别:
1,观察者模式中观察者的响应理论上讲针对特定的被观察者是唯一的(说理论上唯一的原因是,如果你愿意,你完全可以在update方法里添加一系列的elseif去产生不同的响应,但elseif应该尽量避免使用),而事件驱动则不是,因为我们可以定义自己感兴趣的事情,比如刚才,我们可以监听作者发布新书,我们还可以在监听器接口中定义其它的行为。再比如tomcat中,我们可以监听servletcontext的init动作,也可以监听它的destroy动作。
2,虽然事件驱动模型更加灵活,但也是付出了系统的复杂性作为代价的,因为我们要为每一个事件源定制一个监听器以及事件,这会增加系统的负担,各位看看tomcat中有多少个监听器和事件类就知道了。
3,另外观察者模式要求被观察者继承Observable类,这就意味着如果被观察者原来有父类的话,就需要自己实现被观察者的功能,当然,这一尴尬事情,我们可以使用适配器模式弥补,但也不可避免的造成了观察者模式的局限性。事件驱动中事件源则不需要,因为事件源所维护的监听器列表是给自己定制的,所以无法去制作一个通用的父类去完成这个工作。
4,被观察者传送给观察者的信息是模糊的,比如update中第二个参数,类型是Object,这需要观察者和被观察者之间有约定才可以使用这个参数。而在事件驱动模型中,这些信息是被封装在Event当中的,可以更清楚的告诉监听器,每个信息都是代表的什么。

模拟jsp当中的一个事件驱动模型,就是按钮的点击事件。
这个模型当中,按钮自然就是事件源,而事件的种类有很多,比如点击(click),双击(dblclick),鼠标移动事件(mousemove)。我们的监听器与事件个数是一样的,所以这也是事件驱动的弊端,我们需要一堆事件和监听器。
三种事件:

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元素,在这儿只添加个别属性,其余属性同理
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;
    }

}

模拟编写一个页面,这个页面可以当做是一个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;
    }

}

测试类:

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();//接着客户点了提交
    }
}

图片描述
由上述例子可以看出,二者都是用来处理变化与响应的问题,其中观察者更多的是发布-订阅,也就是类似读者和作者的关系,而事件驱动更多的是为了响应客户的请求,从而制定一系列的事件和监听器,去处理客户的请求与操作。
二者其实都是有自己的弱项的,只有掌握了模式的弱项才能更好的使用。
观察者模式所欠缺的是设计上的问题,即观察者和被观察者是多对一的关系,那么反过来的话,就无法支持了。
还有一个问题,每一个观察者都要实现观察者接口,才能添加到被观察者的列表当中,假设一个观察者已经存在,而且我们无法改变其代码,那么就无法让它成为一个观察者了,不过这个我们依然可以使用适配器模式解决。但是还有一个问题就不好解决了,就是假如我们很多类都是现成的,当被观察者发生变化时,每一个观察者都需要调用不同的方法,那么观察者模式就有点捉襟见肘的感觉了,我们必须适配每一个类去统一他们变化的方法名称为update,这是一个很可怕的事情。
对于事件驱动就没有这样的问题,我们可以实现多个监听器来达到监听多个事件源的目的。但是它的缺点是在事件源或者事件增加时,监听器和事件类通常情况下会成对增加,造成系统的复杂性增加,不过目前看来,事件驱动模型一般都比较稳定,所以这个问题并不太明显,因为很少见到无限增加事件的情况发生。还有一个缺点就是我们的事件源需要看准时机触发自己的各个监听器,这也从某种意义上增加了事件源的负担,造成了类一定程度上的臃肿。
最后,二者针对的业务场景概述。
观察者模式:发布(release)--订阅(subscibe),变化(change)--更新(update)
事件驱动模型:请求(request)--响应(response),事件发生(occur)--事件处理(handle)

ps:观察者接口:

//观察者接口,每一个观察者都必须实现这个接口
public interface Observer {
//这个方法是观察者在观察对象产生变化时所做的响应动作,从中传入了观察的对象和一个预留参数
void update(Observable o, Object arg);

}

点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
27
获赞与收藏
233

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消