概述
觀察者模式(有時又被稱為發布/訂閱模式)是軟體設計模式的一種。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實作事件處理系統。
先看一個例子
有個小孩在睡覺,醒來之後要餵奶。
我們使用的是java,所以不要鬧出下面的笑話(披著物件導向的面向過程):
public class Simulation {public static void main(String... args) {//小孩睡覺//起來之後爸爸餵奶//...}}
我們根據物件導向思想,加上多線程類比Child和Dad,小孩在睡覺,隨時可以起來,Dad隔一段時間看下小孩是否醒來。
package observer;import java.util.Random;class Child implements Runnable {public static Random r = new Random();private boolean wake = false;public Child() {new Thread(this).start();}@Overridepublic void run() {while (!wake) {System.out.println("Child:I am sleeping...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (r.nextInt(10) > 8) {wakeUp();}}}public void wakeUp() {wake = true;}public boolean isWake() {return wake;}}class Dad implements Runnable {private Child c;public Dad(Child c) {new Thread(this).start();this.c = c;}@Overridepublic void run() {while (!c.isWake()) {System.out.println("Dad:child is sleeping...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}feed(c);}private void feed(Child c2) {System.out.println("feed child!");}}public class FirstQuestion {public static void main(String[] args) {new Dad(new Child());}}
這樣可以實現功能,但造成資源的浪費。開了這麼多線程,Dad時間都用在看小孩身上了,Dad下午打牌的計劃泡湯了。我們可以很容易的把Dad解放出來:
class Child implements Runnable {public static Random r = new Random();private Dad d;private boolean wake = false;public Child(Dad d) {this.d = d;}@Overridepublic void run() {while (!wake) {System.out.println("Child:I am sleeping...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (r.nextInt(10) > 8) {wakeUp();}}}public void wakeUp() {wake = true;d.feed(this);}public boolean isWake() {return wake;}}class Dad {public void feed(Child c) {System.out.println("feed child!");}}public class FirstQuestion {public static void main(String[] args) {new Thread(new Child(new Dad())).start();}}
這樣基本已經實現功能,稍微完善下,假如想知道小孩什麼時候起來等一些資訊,如果我們寫在小孩類中就不太合適,所以我們抽象出類中WakenUpEvent:
class WakenUpEvent {private Date date;private String loc;private Dad dad;public WakenUpEvent(Date date, String loc, Dad dad) {super();this.date = date;this.loc = loc;this.dad = dad;}}class Child implements Runnable {private Dad d;public Child(Dad d) {this.d = d;}@Overridepublic void run() {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}wakeUp();}public void wakeUp() {d.actionToWakenUp(new WakenUpEvent(new Date(), "child", d));}}class Dad {public void actionToWakenUp(WakenUpEvent wakenUpEvent) {System.out.println("child feed!");}}public class FirstQuestion {public static void main(String[] args) {new Thread(new Child(new Dad())).start();}}
這樣Dad可以做自己事,只要聽到孩子聲音,就過來餵奶。似乎問題已經解決,假如小孩醒後,小孩的爺爺想抱下,小孩家的小狗要叫下,等等,如果按照上面的設計,小孩需要持有爺爺GrandFather、狗Dog等的引用,再調用用響應的處理方法…需要修改較大的篇幅。實際上我們可以在小孩中使用一個集合儲存所有監聽小孩的對象,當小孩醒後,小孩依次調用監聽者處理方法。要實現統一的介面,以可以被小孩監聽器集合引用和調用相應方法,我們使用介面interface。
class WakenUpEvent {private Date date;private String eventType;private Object source;public WakenUpEvent(Date date, String eventType, Object source) {this.date = date;this.eventType = eventType;this.source = source;}}class Child implements Runnable {private List<WakenUpListener> list = new ArrayList<WakenUpListener>();public void addWakenUpListener(WakenUpListener l) {list.add(l);}@Overridepublic void run() {System.out.println("child is sleeping...");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}wakeUp();}public void wakeUp() {for (WakenUpListener l : list) {l.actionWakenUp(new WakenUpEvent(new Date(), "", this));}}}interface WakenUpListener {void actionWakenUp(WakenUpEvent e);}class Dad implements WakenUpListener {@Overridepublic void actionWakenUp(WakenUpEvent e) {System.out.println("dad feed child!");}}class GrandFather implements WakenUpListener {@Overridepublic void actionWakenUp(WakenUpEvent e) {System.out.println("grandfather holl child!");}}public class FirstQuestion {public static void main(String[] args) {Child c = new Child();c.addWakenUpListener(new Dad());c.addWakenUpListener(new GrandFather());new Thread(c).start();}}
這個時候再看概述的例子,比較容易理解了吧!
定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時, 所有依賴於它的對象都得到通知並被自動更新。[GOF 《設計模式》]
觀察者模式中的推模式與拉模式[摘錄]
在Observer模式中區分推模式和拉模式,先簡單的解釋一下兩者的區別:推模式是當有訊息時,把訊息資訊以參數的形式傳遞(推)給所有觀察者,而拉模式是當有訊息時,通知訊息的方法本身並不帶任何的參數,是由觀察者自己到主體對象那兒取回(拉)訊息。知道了這一點,大家可能很容易發現上面我所舉的例子其實是一種推模式的Observer模式。我們先看看這種模式帶來了什麼好處:當有訊息時,所有的觀察者都會直接得到全部的訊息,並進行相應的處理常式,與主體對象沒什麼關係,兩者之間的關係是一種鬆散耦合。但是它也有缺陷,第一是所有的觀察者得到的訊息是一樣的,也許有些資訊對某個觀察者來說根本就用不上,也就是觀察者不能“按需所取”;第二,當通知訊息的參數有變化時,所有的觀察者對象都要變化。鑒於以上問題,拉模式就應運而生了,它是由觀察者自己主動去取訊息,需要什麼資訊,就可以取什麼,不會像推模式那樣得到所有的訊息參數。OK,說到這兒,你是否對於推模式和拉模式有了一點瞭解呢?
實際上上面的代碼中,因java中awt事件的影響,我在Event中加入了source欄位,這算是拉模式的一種體現。我們可以得到公用的事件資訊,也可以通過source得到發出事件對象的資訊。
AWT事件類比
說到AWT事件,我們根據上面的思路類比下awt事件處理,觀察者模式實現awt事件功能更加簡單優雅,然而真正的awt也需要windows本身的事件驅動的支援,比如你按下某個button,首先windows捕獲這個訊息,把訊息分發給java虛擬機器,虛擬機器在調用button相應的處理,button調用監聽器處理(個人理解)。一般awt事件處理:
public class AwtButton extends Frame {public void lanch() {Button b = new Button("test");b.addActionListener(new MyActionListener());b.addActionListener(new MyActionListener1());this.add(b);this.addWindowListener(new WindowAdapter() {@Overridepublic void windowClosing(WindowEvent e) {System.exit(0);}});setSize(100, 100);setVisible(true);}public static void main(String[] args) {new AwtButton().lanch();}class MyActionListener implements ActionListener {@Overridepublic void actionPerformed(ActionEvent e) {System.out.println("button pressed!");}}class MyActionListener1 implements ActionListener {@Overridepublic void actionPerformed(ActionEvent e) {System.out.println("button pressed1!");}}}
結合上面的孩子的例子,我們使用控制台類比awt事件:
public class SimulationAwtButton {public static void main(String[] args) {Button b = new Button();b.addActionListener(new MyActionListener1());b.addActionListener(new MyActionListener2());b.pressed();}}class Button {private List<ActionListener> list = new ArrayList<ActionListener>();public void addActionListener(ActionListener l) {list.add(l);}public void pressed() {ActionEvent e = new ActionEvent(this);for(ActionListener l : list) {l.actionPerform(e);}}}interface ActionListener {void actionPerform(ActionEvent e) ;}class ActionEvent {private long time;private Object source;public ActionEvent(Object source) {this.time = System.currentTimeMillis();this.source = source;}public long getTime() {return time;}public Object getSource() {return source;}}class MyActionListener1 implements ActionListener {@Overridepublic void actionPerform(ActionEvent e) {System.out.println("SimulationButton ActionPerformed:" +e.getTime() + e.getSource());}}class MyActionListener2 implements ActionListener {@Overridepublic void actionPerform(ActionEvent e) {System.out.println("SimulationButton ActionPerformed:" +e.getTime() + e.getSource());}}
這樣,對java送awt事件處理有了更深的認識。
適用性
1.當一個抽象模型有兩個方面, 其中一個方面依賴於另一方面。將這二者封裝在獨立的對象中以使它們可以各自獨立地改變和複用。
2.當對一個對象的改變需要同時改變其它對象, 而不知道具體有多少對象有待改變。
3.當一個對象必須通知其它對象,而它又不能假定其它對象是誰。換言之, 你不希望這些對象是緊密耦合的。
總結
通過Observer模式,把一對多個物件之間的通知依賴關係的變得更為鬆散,大大地提高了程式的可維護性和可擴充性,也很好的符合了開放-封閉原則。
參考資料
.NET設計模式(19):觀察者模式(Observer Pattern)(部分摘錄原文)
java尚學堂馬士兵設計模式
百度百科:觀察者模式