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類型,這樣靈活性更強。
反思與總結
從一開始第一種設計方法的一兩個類實現了基本功能,到最後的多個類、接口,可以看到:使用設計模式是一把雙刃劍。
優點:
- 可擴展性強
- 維護成本降低
缺點:
- 複雜度增加
- 開發成本增加
但是切忌爲了使用設計模式而使用設計模式,要具體問題具體分析。