观察者模式小试

观察者模式小试

观察者模式又叫订阅-发布模式,也是非常常用的设计模式之一。

一、介绍

还是先来看一下《研磨设计模式》的介绍——定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变的时候,所有依赖于它的对象都得到通知,并被自动更新。

观察者模式的本质:触发联动。

什么意思呢?说白了,就是说一个对象的状态发生改变,另一个对象自动做出响应。怎样能够使一个目标对象的状态发生改变时,观察者对象自动做出响应呢?

很简单,让目标对象持有观察者对象就可以了。如果一个目标对象有多个观察者,每次目标对象的状态改变,就自动遍历自己持有的观察者对象,将自己状态改变的情况通知观察者,就是传递参数给观察者。观察者模式无非就是这样。

参看我的博文中介者模式小试,可知道,观察者模式和中介者模式有很多相似的地方。组件间传递信息时,也经常将自身传过去,然后处理时使用强制类型转换进行处理。不过中介者一般是多个组件类将自身传递给代理类,让代理类统一处理组件间的交互。而观察者模式则是反过来,目标类发生改变时,一般将自身传递给自己持有的每个观察者,这样就激活了观察者的方法。

二、我的实现

Swing中包含了大量的观察者模式的实现。为了便于理解,在这里我也模仿Swing。我们都知道在画板上画画,每次我们在画板上点击鼠标左键,马上,画板上上就出现了相应的点。拖住不放,就可以画出一条线。这是为什么呢?我们假设画板作为目标对象,有一个监听器在监听。每次点击左键都会产生一个事件,画板会马上接受这样一个事件,然后处理之后传给监听器,监听器把它画出来。

我要模拟的就是这个过程。如下:

1、一个抽象的目标类:

//抽象的目标类
public abstract class Subject {

    //监听器列表
    protected List<Listener> listenerList = new ArrayList<Listener>();

    //添加监听器
    public void addListener(Listener listener)
    {
        listenerList.add(listener);
    }

    //移除监听器
    public void removeListener(Listener listener)
    {
        listenerList.remove(listener);
    }

    //通知所有监听器
    abstract void notifyListener();
}

2、监听器只是一个标识接口,不实现任何方法:

public interface Listener {
}

3、将触发事件的因素封装起来,成为一个Event类,鼠标事件如下:

//模拟鼠标事件
public class MouseEvent {

    //模拟鼠标左、中、右键
    public static final int BUTTON1 = 1;
    public static final int BUTTON2 = 2;
    public static final int BUTTON3 = 3;
    private int x;
    private int y;
    private int ClickCount;
    
    public int getClickCount()
    {
        return ClickCount;
    }

    public void setClickCount(int clickCount)
    {
        ClickCount = clickCount;
    }

    public int getX()
    {
        return x;
    }

    public void setX(int x)
    {
        this.x = x;
    }

    public int getY()
    {
        return y;
    }

    public void setY(int y)
    {
        this.y = y;
    }

}

4、每次都需要从鼠标事件中取出鼠标位置,封装成屏幕上的点,PointOnScreen类,如下:

public class PointOnScreen {

    private int x;
    private int y;

    public int getX()
    {
        return x;
    }

    public void setX(int x)
    {
        this.x = x;
    }

    public int getY()
    {
        return y;
    }

    public void setY(int y)
    {
        this.y = y;
    }

}

5、系统监听器,实现了标识接口Listener:

//系统监听器
public class SystemListener implements Listener {
    // 在屏幕上将这个图形画出来
    public void drawOnScreen(List<PointOnScreen> graph)
    {
        System.out.println();
        System.out.println("刷新时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
        for (PointOnScreen point : graph)
        {
            System.out.println("当前画到的点是——————(" + point.getX() + "," + point.getY() + ")");
        }
    }
}

6、下面是最重要的目标具体类——画板类:

public class Panel extends Subject {

    // 表示图形
    private List<PointOnScreen> graph = new ArrayList<PointOnScreen>();

    // 添加鼠标事件
    public void addKeyEvent(MouseEvent event)
    {
        // 处理鼠标事件
        PointOnScreen point = new PointOnScreen();
        point.setX(event.getX());
        point.setY(event.getY());
        graph.add(point);
        // 通知所有监听器
        notifyListener();
    }

    @Override
    void notifyListener()
    {
        // 遍历每一个PanelListener,画图!
        for (Listener listener : listenerList)
        {
            if (listener instanceof SystemListener)
            {
                ((SystemListener) listener).drawOnScreen(graph);
            }
        }
    }

}

7、至此,已经完成,我们来测试一下:

public class Test {

    public static void main(String[] args)
    {
        //创建MouseEvent,几个鼠标事件
        MouseEvent event1 = new MouseEvent();
        event1.setX(1);
        event1.setY(2);
        MouseEvent event2 = new MouseEvent();
        event2.setX(2);
        event2.setY(2);
        MouseEvent event3 = new MouseEvent();
        event3.setX(2);
        event3.setY(3);
        
        //创建目标类
        Panel panel = new Panel();
        
        //系统监听器
        SystemListener autoListener = new SystemListener();
        
        //注册监听器
        panel.addListener(autoListener);
        
        //添加事件,模拟鼠标点击操作
        panel.addKeyEvent(event1);
        panel.addKeyEvent(event2);
        panel.addKeyEvent(event3);
    }
}

8、结果如下:

刷新时间:2014-04-29 15:44:26.546
当前画到的点是——————(1,2)

刷新时间:2014-04-29 15:44:26.548
当前画到的点是——————(1,2)
当前画到的点是——————(2,2)

刷新时间:2014-04-29 15:44:26.548
当前画到的点是——————(1,2)
当前画到的点是——————(2,2)
当前画到的点是——————(2,3)

如上,已经模拟出了画图的过程。

三、推模型和拉模型

什么是推模型和拉模型呢?上面例子中,事件源是什么呢?是MouseEvent。可是传给监听器对象的时候,我们是将MouseEvent包装成PointOnScreen对象去传递的。这就是推模型。

相对的,拉模型指的就是,传递信息给监听器的时候,将本身的引用传递过去,那么监听器对象希望处理什么信息就处理什么信息,那就是拉模型。

我们将Panel改变一下:

public class Panel extends Subject {

    // 表示图形
    private List<PointOnScreen> graph = new ArrayList<PointOnScreen>();

    private KeyEvent keyEvent;
    
    public KeyEvent getKeyEvent(){
        return keyEvent;
    }
    
    // 添加鼠标事件
    public void addKeyEvent(MouseEvent event)
    {
        // 处理鼠标事件
        PointOnScreen point = new PointOnScreen();
        point.setX(event.getX());
        point.setY(event.getY());
        graph.add(point);
        // 通知所有监听器
        notifyListener();
    }

    void notifyListener(){
        for (Listener listener : listenerList)
        {
            if (listener instanceof SystemListener)
            {
                //将自身对象传过去
                listener.update(this);
            }
        }
    }
}

然后,把SystemListener变成这样:

//系统监听器
public class SystemListener implements Listener {
    // 在屏幕上将这个图形画出来
    public void drawOnScreen(List<PointOnScreen> graph)
    {
        System.out.println();
        System.out.println("刷新时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
        for (PointOnScreen point : graph)
        {
            System.out.println("当前画到的点是——————(" + point.getX() + "," + point.getY() + ")");
        }
    }

    // 表示图形
    private List<PointOnScreen> graph = new ArrayList<PointOnScreen>();

    public void update(Subject subject)
    {
        if (subject instanceof Panel)
        {
            MouseEvent event = ((Panel) subject).getMouseEvent();
            // 处理鼠标事件
            PointOnScreen point = new PointOnScreen();
            point.setX(event.getX());
            point.setY(event.getY());
            graph.add(point);
            drawOnScreen(graph);
        }
    }

}

如上,目标对象传递信息的时候将自身传递过去,监听器处理信息的时候,需要什么就从目标对象哪里拿什么,非常方便。这就是拉模型。

四、Java中的观察者模式

要实现观察者模式,其实完全不用那么麻烦,目标类和监听者接口Java已经帮我们实现了。目标类是java.util.Observable,监听器接口是java.util.Observer

即,目标类要继承java.util.Observable,监听器接口实现java.util.Observer。怎么传值呢?

目标类状态改变之后,调用这样几个方法:

        //状态改变
        this.keyEvent = event;
        //状态改变了,不可少
        this.setChanged();
        // 拉模型
        this.notifyObservers();
        //推模型所传的对象
        this.notifyObservers(graph);    

同时,监听器接口实现了这样一个方法:

  public void update(Observable o, Object arg)
    {
        //拉模型处理o
        //推模型处理arg
    }

非常简单!

下面用Java的观察者模式实现示例

1、目标类如下:

public class Panel extends java.util.Observable {

    // 表示图形,用于推模型
    private List<PointOnScreen> graph = new ArrayList<PointOnScreen>();
    
    private MouseEvent keyEvent;
    
    public MouseEvent getMouseEvent(){
        return keyEvent;
    }
    
    // 添加鼠标事件
    public void addKeyEvent(MouseEvent event)
    {   //状态改变
        this.keyEvent = event;
        //状态改变了,不可少
        this.setChanged();
        // 拉模型
        this.notifyObservers();
        
        // 推模型,处理鼠标事件
        PointOnScreen point = new PointOnScreen();
        point.setX(event.getX());
        point.setY(event.getY());
        graph.add(point);
        //推模型所传的对象
        this.notifyObservers(graph);
    }
}

2、监听器类如下:

//系统监听器
public class SystemListener  implements java.util.Observer {
    // 在屏幕上将这个图形画出来
    public void drawOnScreen(List<PointOnScreen> graph)
    {
        System.out.println();
        System.out.println("刷新时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
        for (PointOnScreen point : graph)
        {
            System.out.println("当前画到的点是——————(" + point.getX() + "," + point.getY() + ")");
        }
    }

    // 表示图形
    private List<PointOnScreen> graph = new ArrayList<PointOnScreen>();

    @Override
    public void update(Observable o, Object arg)
    {
        if (o instanceof Panel)
            
        {
            MouseEvent event = ((Panel) o).getMouseEvent();
            // 处理鼠标事件
            PointOnScreen point = new PointOnScreen();
            point.setX(event.getX());
            point.setY(event.getY());
            graph.add(point);
            drawOnScreen(graph);
        }
        
        //推模型
        List<PointOnScreen> graph = (ArrayList<PointOnScreen>)arg;
        drawOnScreen(graph);
    }

}