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
这个类里面, 属性除了有time
和location
之外还有一个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类型,这样灵活性更强。
反思与总结
从一开始第一种设计方法的一两个类实现了基本功能,到最后的多个类、接口,可以看到:使用设计模式是一把双刃剑。
优点:
- 可扩展性强
- 维护成本降低
缺点:
- 复杂度增加
- 开发成本增加
但是切忌为了使用设计模式而使用设计模式,要具体问题具体分析。