設計模式之觀察者模式簡單理解
本篇文章總結於馬士兵的視訊教程《觀察者模式》。個人非常推薦馬士兵的視訊教程,對於初學Java的人來說,J2SE基礎視訊非常不錯,對於記憶體分析講的十分到位。對於有一定基礎的人來說,設計模式系列,反射系列,正則表示式系列都非常不錯,不僅僅侷限於Java,而C#學習者也可以看一看。
這裡通過一個生活例子來講觀察者模式。這個場景就是,有一個小孩在睡覺,然後他老爸在旁邊,小孩醒了他老爸就要喂他吃東西。從面相物件的角度來講,我們通過描述就可以抽象出實體類,小孩,老爸是兩個物件,然後睡覺是小孩的狀態,喂東西是老爸的方法。
第一種方法,我們用最直接的方式去模擬這個過程。當小孩在睡覺的時候,老爸會一直的去檢視小孩是否醒了,
package com.robin.test; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { Baby b = new Baby(); Dad d = new Dad(b); new Thread(d).start(); new Thread(b).start(); System.in.read(); } } class Baby implements Runnable { private boolean wakeUp = false; public boolean isWakeUp() { return wakeUp; } void wakeUp() { wakeUp = true; } @Override public void run() { try { Thread.sleep(1000 * 10); this.wakeUp(); } catch (Exception ex) { ex.printStackTrace(); } } } class Dad implements Runnable { Baby baby; private void feedBaby() { System.out.println("Feed the baby..." + baby.toString()); } public Dad(Baby baby) { this.baby = baby; } @Override public void run() { while(true) { if(baby.isWakeUp() == true) { feedBaby(); } } } }
這種方式存在一定的弊端,即老爸得時時刻刻盯著小孩,這個過程中不能幹別的事情,即使客廳裡有足球賽,這時候老爸也不能把小孩放在房間裡自己去客廳。反應到程式中來講,這種死迴圈的方式,對於CPU是一種無端的消耗。
接下來我們用第二種方式進行模擬。我們可以這樣,我們讓小孩持有老爸的引用,當小孩醒了之後,主動呼叫老爸的feed方法。就相當於在小孩與老爸之間綁了一根繩子,小孩在裡屋睡覺,老爸在客廳看球,當小孩醒了之後,拽動這個繩子,那麼老爸得知訊息後再進裡屋來喂東西。程式碼可以如下。
package com.robin.test; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { Dad d = new Dad(); Baby b = new Baby(d); new Thread(b).start(); System.in.read(); } } class Baby implements Runnable { private boolean wakeUp = false; private Dad dad; public boolean isWakeUp() { return wakeUp; } void wakeUp() { wakeUp = true; } public Baby(Dad dad) { this.dad = dad; } @Override public void run() { try { Thread.sleep(1000 * 10); this.wakeUp(); dad.feedBaby(); } catch (Exception ex) { ex.printStackTrace(); } } } class Dad { public void feedBaby() { System.out.println("Feed the baby..."); } }
其實,到這種實現方法,我們已經引入了觀察者模式。觀察者模式的核心就是讓行為的行使著變主動為被動。行為的行使著就是觀察者。但是從面向物件的角度來講,有些地方我們還可以接著完善。比如說,小孩什麼時候醒的,這個屬性不是小孩的,也不是老爸的,而是發生的這件事情本身的,所以醒來這件事應該也是一個類。所以接下來,我們完善一下程式碼,用第三種方式實現。
package com.robin.test;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Dad d = new Dad();
Baby b = new Baby(d);
new Thread(b).start();
System.in.read();
}
}
class Baby implements Runnable {
private boolean wakeUp = false;
private Dad dad;
public boolean isWakeUp() {
return wakeUp;
}
void wakeUp() {
wakeUp = true;
}
public Baby(Dad dad) {
this.dad = dad;
}
@Override
public void run() {
try {
Thread.sleep(1000 * 10);
this.wakeUp();
WakeUpEvent wakeUpEvent = new WakeUpEvent(System.currentTimeMillis(), "Home");
dad.ActionToWakeUp(wakeUpEvent);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
class Dad {
public void ActionToWakeUp(WakeUpEvent event) {
System.out.println("Feed the baby..." + event.getHappenTime() + event.getLocation());
}
}
class WakeUpEvent {
private long happenTime;
public long getHappenTime() {
return happenTime;
}
public String getLocation() {
return location;
}
private String location;
public WakeUpEvent(long happenTime, String location) {
this.happenTime = happenTime;
this.location = location;
}
}
這裡如果我們要求小孩的老媽也要對小孩的醒來進行一個些反應,我們就要修改小孩內部的程式碼,讓小孩同時也持有老媽的引用。面向物件的一個重要原則就是新增而不是修改,就是說可以向小孩新增監聽者,而不應該修改小孩的內部程式碼。所以,現在我們可以讓所有的監聽者實現同一個介面。而小孩內部持有這個介面的集合,那麼就可以新增無數的監聽者了。
package com.robin.test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
Dad d = new Dad();
Mom m = new Mom();
Baby b = new Baby();
b.getList().add(m);
b.getList().add(d);
new Thread(b).start();
System.in.read();
}
}
class Baby implements Runnable {
private boolean wakeUp = false;
private List<BabyObserver> list;
public List<BabyObserver> getList() {
return list;
}
public boolean isWakeUp() {
return wakeUp;
}
void wakeUp() {
wakeUp = true;
}
public Baby() {
this.list = new ArrayList<BabyObserver>();
}
@Override
public void run() {
try {
Thread.sleep(1000 * 10);
this.wakeUp();
WakeUpEvent wakeUpEvent = new WakeUpEvent(System.currentTimeMillis(), "Home");
for(int i=0; i<list.size(); i++) {
BabyObserver observer = list.get(i);
observer.ActionToWakeUp(wakeUpEvent);
}
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
interface BabyObserver {
void ActionToWakeUp(WakeUpEvent event);
}
class Dad implements BabyObserver{
@Override
public void ActionToWakeUp(WakeUpEvent event) {
System.out.println("Dad Feed the baby..." + event.getHappenTime() + event.getLocation());
}
}
class Mom implements BabyObserver{
@Override
public void ActionToWakeUp(WakeUpEvent event) {
System.out.println("Mom Feed the baby..." + event.getHappenTime() + event.getLocation());
}
}
class WakeUpEvent {
private long happenTime;
public long getHappenTime() {
return happenTime;
}
public String getLocation() {
return location;
}
private String location;
public WakeUpEvent(long happenTime, String location) {
this.happenTime = happenTime;
this.location = location;
}
}
所以,最後這版的程式碼既實現了觀察者模式,又有良好的可擴充套件性。綜合來講,使用觀察者模式最大的好處就是減少死迴圈式的輪循帶來的資源無端消耗,並且有著良好的可擴充套件性。
最後,說一點Java自身一個典型的觀察者模式的實現,那就是AWT的Button等GUI的事件監聽機制。對於滑鼠點選或者鍵盤輸入,Windows作業系統採用的是一種事件派發的機制,作業系統有一個GUI的監聽程序,這個程序會不斷的監聽硬體給的反饋,比如說滑鼠點選了一下,或者鍵盤按了那個字母鍵。這種行為被作業系統的這個程序捕捉到了以後,它檢查到這個事件是哪個具體的程式的,比如說是個AWT程式的,作業系統會告訴虛擬機器,滑鼠按下了,你進行處理吧。虛擬機器有個專門的執行緒來進行事件處理,這個執行緒擁有所有的AWT的GUI物件,比如說Button等,都擁有其引用。這個執行緒接到作業系統的訊號後,會把這些資訊封裝成為一個ActionEvent類,然後檢查一下點選了哪個元件,發現是button,就會呼叫button註冊的那些方法。而button註冊的那些方法,都是實現了同一個介面,所以button中有一個list,可以註冊無數多的介面,而這些介面的具體內容可以由我們自己實現。這種設計有非常好的可擴充套件性。
綜合上邊,就是button作為一個觀察者,沒有時時刻刻監聽著滑鼠與鍵盤事件,也沒有時時刻刻死迴圈的去詢問作業系統有沒有針對自己的點選,而是把自己的引用交給了虛擬機器專門的GUI執行緒,而自己該幹嘛幹嘛,沒有阻塞到這裡。