设计模式之Observer

来源:互联网 发布:剑灵可爱召唤捏脸数据 编辑:程序博客网 时间:2024/06/05 14:11

Observer

tags: design pattern,Observer


要求:

模拟以下情景:

小孩在睡觉
醒了之后要吃东西

第一种设计方法

(说实话这是我第一反应想到的方法,我果然还是图样图森破。。。)

有一个Dad类, 有一个Child类, Dad类持有Child类的引用, Dad监测着Child, 如果Child醒了, Dad就调用feed方法去喂小孩。

package simulation;class Child implements Runnable {    private boolean wakeUp = false;    public void wakeUp() {        this.wakeUp = true;    }    public boolean isWakeUp() {        return this.wakeUp;    }    @Override    public void run() {        try {            Thread.sleep(5000);        } catch (InterruptedException e) {            e.printStackTrace();        }        this.wakeUp();    }}class Dad implements Runnable {    private Child child;    public Dad(Child c) {        this.child = c;    }    @Override    public void run() {        while (!child.isWakeUp()) {            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        this.feed(this.child);    }    public void feed(Child c) {        System.out.println("feed child");    }}public class Test {    public static void main(String[] args) {        Child c = new Child();        new Thread(new Dad(c)).start();    }}

分析:
程序可行是可行, 但是有极其不合理的地方:Dad每隔一秒钟看一下Child, 完全干不了别的事。–> CPU的资源被无端消耗, 上面的代码在效率和资源消耗上都有很大问题!!!

那么应该如何改进呢?

第二种设计方法:

化主动为被动!

把Dad主动监测Child变为被动监测, 就是说, 反过来, 让Child监测Dad。换句话说, 在Child睡觉的时候Dad可以干别的事, 但Child一醒过来,Dad马上过来喂他吃东西。

这时候把上面的代码修改成下面的:

package simulation;class Child implements Runnable {    private Dad dad;    // private boolean wakeUp = false;    public Child(Dad d) {        this.dad = d;    }    public void wakeUp() {        // this.wakeUp = true;        this.dad.feed(this);    }    // public boolean isWakeUp() {    // return this.wakeUp;    // }    @Override    public void run() {        try {            Thread.sleep(5000);        } catch (InterruptedException e) {            e.printStackTrace();        }        this.wakeUp();    }}class Dad {    public void feed(Child c) {        System.out.println("feed child");    }}public class Test {    public static void main(String[] args) {        Dad d = new Dad();        Child c = new Child(d);        new Thread(c).start();    }}

这时候Dad完全可以不用作为一个线程类, Dad这个类里也可以只保留feed这个方法, Child中持有Dad的引用, Child一醒过来, Dad就调用feed方法。

这样修改之后, 明显比第一种方法更具有效率。

但是作为设计来讲, 在一个程序当中, 如果只考虑当前而没有预料到将来一定时间内将会发生的变化, 那么程序不具有可扩展性,弹性很差。

比如上面这个情景, Child醒过来这件事,包含了许多信息:几点醒过来?是在早上还是在晚上?
睡了多久?在哪里醒过来?等等。 针对不同的事件信息,作为监测者的Dad应该有不同的处理方式, 不能说一醒过来就喂, 如果是晚上Child刚吃完饭睡了一下, 醒过来, Dad又喂他吃东西, 那么Child就撑死了。

所以第二种方法仅仅是把程序写通,可扩展性很差。

对于事件的处理

Child醒过来这件事的发生包含了许多具体情况(具体信息),应该把这些情况告诉监测者, 也就是Dad, Dad根据这件事情的具体情况, 来做出具体的处理方式。

因此把事件抽象出来,封装成另外一个类。

package simulation;class WakeUpEvent {    private long time;    private String location;    private Object source;    public WakeUpEvent(long time, String location, Object source) {        this.time = time;        this.location = location;        this.source = source;    }    public long getTime() {        return time;    }    public void setTime(long time) {        this.time = time;    }    public String getLocation() {        return location;    }    public void setLocation(String location) {        this.location = location;    }    public Object getSource() {        return source;    }    public void setSource(Object source) {        this.source = source;    }}class Child implements Runnable {    private Dad dad;    // private boolean wakeUp = false;    public Child(Dad d) {        this.dad = d;    }    public void wakeUp() {        // this.wakeUp = true;        this.dad.actionToWakeUp(new WakeUpEvent(System.currentTimeMillis(),                "bed", this));    }    // public boolean isWakeUp() {    // return this.wakeUp;    // }    @Override    public void run() {        try {            Thread.sleep(5000);        } catch (InterruptedException e) {            e.printStackTrace();        }        this.wakeUp();    }}class Dad {    public void actionToWakeUp(WakeUpEvent event) {        // do something according to the event    }}public class Test {    public static void main(String[] args) {        Dad d = new Dad();        Child c = new Child(d);        new Thread(c).start();    }}

注意上面WakeUpEvent这个类里面, 属性除了有timelocation之外还有一个source, 而且类型是Object(其实也可以写成Child类型, 但为了更像AWT, 所以写成Object),这里表示一个事件源对象,就是发生这件事的对象。比如说Child醒了这个事件, Child就是事件源。

然后再思考:Child一醒过来Dad就要喂他, 那么喂这个动作就已经被固定下来了。假如Child醒来之后不想让Dad喂他, 而是想让Dad抱他出去玩,那么明显feed这个方法已经不合适了。更灵活的方法是:Child一醒过来, 发生了这么一件事, Dad便对这件事做出反应,至于是喂他还是抱他出去玩都可以。

所以把Dad这个类中原来的feed方法修改了,方法名改成了actionToWakeUp,参数改成了WakeUpEvent event 在方法体内, 便可以增加判断,根据事件的不同具体信息做出不同的反应。这样写明显比使用feed方法灵活得多。

继续思考:如果现在不只有Dad, Child醒过来之后,他的Grandpa也要做出反应,那么应该怎么办呢?按照之前的思路, 增加一个Grandpa类, 里面也有一个actionToWakeUp(WakeUpEvent event)方法,另外在Child这个类里增加一个Grandpa的引用。那么如果现在不仅是爸爸、爷爷对小孩醒过来做出反应,小孩的妈妈、奶奶、外公、外婆甚至是家里的狗都要做出反应呢?那岂不是要不断的修改Child这个类的源代码?!在OO里面有一个极其重要的核心原则————OCP, open close principle, 开闭原则, 对扩展开放, 对修改关闭。上面的方法要不断修改Child的源代码,显然是不符合这个原则的, 说明了设计还不到位!

第三种设计方法 Observer

现在有好多好多监测小孩醒过来这件事的人,而且每个人对这件事的响应各不相同,但是有一个共同点,可以把响应的方法都叫actionToWakeUp(WakeUpEvent event),参数都是小孩醒过来这件事。这时候可以考虑使用接口把变化的这部分给抽象出来,因为接口抽象出来的是一系列的类所具有的共同特点。
因此增加一个WakeUpListener的接口,里面有actionToWakeUp(WakeUpEvent event)这个方法。 然后让Dad和Grandpa这两个类实现这个接口。

这时候再思考一下,为什么这个方法里传的参数是WakeUpEvent而不是Child? 假如写成Child, 那么这个方法就只能用在小孩身上了,但是如果是WakeUpEvent,那么小狗醒了也可以用这个方法,小猫醒了也可以用这个方法,也就是说, 事件本身也是和事件源是脱离的。这个时候灵活性最高!

把上面的代码修改:

package simulation;import java.util.*;class WakeUpEvent {    private long time;    private String location;    private Object source;    public WakeUpEvent(long time, String location, Object source) {        this.time = time;        this.location = location;        this.source = source;    }    public long getTime() {        return time;    }    public void setTime(long time) {        this.time = time;    }    public String getLocation() {        return location;    }    public void setLocation(String location) {        this.location = location;    }    public Object getSource() {        return source;    }    public void setSource(Object source) {        this.source = source;    }}class Child implements Runnable {    private List<WakeUpListener> listeners = new ArrayList<WakeUpListener>();    public void addWakeUpListener(WakeUpListener listener) {        this.listeners.add(listener);    }    public void wakeUp() {        for (Iterator<WakeUpListener> it = this.listeners.iterator(); it                .hasNext();) {            WakeUpListener l = it.next();            l.actionToWakeUp(new WakeUpEvent(System.currentTimeMillis(), "bed",                    this));        }    }    // public boolean isWakeUp() {    // return this.wakeUp;    // }    @Override    public void run() {        try {            Thread.sleep(5000);        } catch (InterruptedException e) {            e.printStackTrace();        }        this.wakeUp();    }}interface WakeUpListener {    public void actionToWakeUp(WakeUpEvent event);}class Dad implements WakeUpListener {    public void actionToWakeUp(WakeUpEvent event) {        System.out.println("feed child");    }}class Grandpa implements WakeUpListener {    public void actionToWakeUp(WakeUpEvent event) {        System.out.println("hug child");    }}public class Test {    public static void main(String[] args) {        Dad d = new Dad();        Child c = new Child();        c.addWakeUpListener(d);        new Thread(c).start();    }}

注意:这时候Child这个类里没有Dad或者Grandpa的引用, 而是改成了一个List<WakeUpListener>, 并且多了一个addWakeUpListener(WakeUpListener listener)的方法,当需要添加监听器的时候,添加一个实现了WakeUpListener这个接口的类, 在main方法里new一个新的监听器对象,然后直接调用这个方法添加便可以,完全无需修改Child的代码。扩展程序而无需修改Child的源代码, 这才符合OCP原则!

比如我还想添加一个小狗, 他也在监听着小孩醒来这件事:

package simulation;import java.util.*;class WakeUpEvent {    private long time;    private String location;    private Object source;    public WakeUpEvent(long time, String location, Child source) {        this.time = time;        this.location = location;        this.source = source;    }    public long getTime() {        return time;    }    public void setTime(long time) {        this.time = time;    }    public String getLocation() {        return location;    }    public void setLocation(String location) {        this.location = location;    }    public Object getSource() {        return source;    }    public void setSource(Object source) {        this.source = source;    }}class Child implements Runnable {    private List<WakeUpListener> listeners = new ArrayList<WakeUpListener>();    public void addWakeUpListener(WakeUpListener listener) {        this.listeners.add(listener);    }    public void wakeUp() {        for (Iterator<WakeUpListener> it = this.listeners.iterator(); it                .hasNext();) {            WakeUpListener l = it.next();            l.actionToWakeUp(new WakeUpEvent(System.currentTimeMillis(), "bed",                    this));        }    }    // public boolean isWakeUp() {    // return this.wakeUp;    // }    @Override    public void run() {        try {            Thread.sleep(5000);        } catch (InterruptedException e) {            e.printStackTrace();        }        this.wakeUp();    }}interface WakeUpListener {    public void actionToWakeUp(WakeUpEvent event);}class Dad implements WakeUpListener {    public void actionToWakeUp(WakeUpEvent event) {        System.out.println("feed child");    }}class Grandpa implements WakeUpListener {    public void actionToWakeUp(WakeUpEvent event) {        System.out.println("hug child");    }}class Dog implements WakeUpListener {    public void actionToWakeUp(WakeUpEvent event) {        System.out.println("Wang!!!!");    }}public class Test {    public static void main(String[] args) {        Dad d = new Dad();        Grandpa g = new Grandpa();        Child c = new Child();        c.addWakeUpListener(d);        c.addWakeUpListener(g);        Dog dog = new Dog();        c.addWakeUpListener(dog);        new Thread(c).start();    }}

由上面的代码可以看见, 只是新建了一个Dog的类, 然后再在main方法里添加了

        Dog dog = new Dog();        c.addWakeUpListener(dog);

这两句话,其他的完全没有修改。主要的逻辑类Child完全没有变,可扩展性很高!

再进一步思考!假如我现在有一个Student类,他也可以发出WakeUpEvent这件事,那么我只需要复制Child里的代码到Student这个类里面就可以了,WakeUpEvent这个类也得到了复用,灵活性更高!!!想想AWT里面,除了Button这个类会发出ActionEvent这件事, TextField也会, 因此ActionEvent也被重用了。在这里,Button相当于Child, Textfield相当于Student, ActionEvent相当于WakeUpEvent。

另外还可以封装一个CryEvent类,HappyEvent类等等各种各样的event,然后再封装一个abstract class Event,让各种event从这个抽象类继承,方法传参数的时候形参定义为Event类型,这样灵活性更强。

反思与总结

从一开始第一种设计方法的一两个类实现了基本功能,到最后的多个类、接口,可以看到:使用设计模式是一把双刃剑。

  • 优点:

    1. 可扩展性强
    2. 维护成本降低
  • 缺点:

    1. 复杂度增加
    2. 开发成本增加

但是切忌为了使用设计模式而使用设计模式,要具体问题具体分析

0 0
原创粉丝点击