一、 引子
還記得警匪片上,匪徒們是怎么配合實施犯罪的嗎?一個團伙在進行盜竊的時候,總 ??? 有一兩個人在門口把風——如果有什么風吹草動,則會立即通知里面的同伙緊急撤退。也許放風的人并不一定認識里面的每一個同伙;而在里面也許有新來的小弟不認識這個放風的。但是這沒什么,這個影響不了他們之間的通訊,因為他們之間有早已商定好的暗號。
呵呵,上面提到的放風者、偷竊者之間的關系就是觀察者模式在現實中的活生生的例子。
?
二、 定義與結構
觀察者( Observer )模式又名發布 - 訂閱( Publish/Subscribe )模式。 GOF 給觀察者模式如下定義:定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都得到通知并被自動更新。
在這里先講一下面向對象設計的一個重要原則——單一職責原則。因此系統的每個對象應該將重點放在問題域中的離散抽象上。因此理想的情況下,一個對象只做一件事情。這樣在開發中也就帶來了諸多的好處:提供了重用性和維護性,也是進行重構的良好的基礎。
因此幾乎所有的設計模式都是基于這個基本的設計原則來的。觀察者模式的起源我覺得應該是在 GUI 和業務數據的處理上,因為現在絕大多數講解觀察者模式的例子都是這一題材。但是觀察者模式的應用決不僅限于此一方面。
下面我們就來看看觀察者模式的組成部分。
1) ??????? 抽象目標角色( Subject ):目標角色知道它的觀察者,可以有任意多個觀察者觀察同一個目標。并且提供注冊和刪除觀察者對象的接口。目標角色往往由抽象類或者接口來實現。
2) ??????? 抽象觀察者角色( Observer ):為那些在目標發生改變時需要獲得通知的對象定義一個更新接口。抽象觀察者角色主要由抽象類或者接口來實現。
3) ??????? 具體目標角色( Concrete Subject ):將有關狀態存入各個 Concrete Observer 對象。當它的狀態發生改變時 , 向它的各個觀察者發出通知。
4) ??????? 具體觀察者角色( Concrete Observer ):存儲有關狀態,這些狀態應與目標的狀態保持一致。實現 Observer 的更新接口以使自身狀態與目標的狀態保持一致。在本角色內也可以維護一個指向 Concrete Subject 對象的引用。
放上觀察者模式的類圖,這樣能將關系清晰的表達出來。
?????? 可以看得出來,在 Subject 這個抽象類中,提供了上面提到的功能,而且存在一個通知方法: notify 。還可以看出來 Subject 和 ConcreteSubject 之間可以說是使用了模板模式(這個模式真是簡單普遍到一不小心就用到了)。
?????? 這樣當具體目標角色的狀態發生改變,按照約定則會去調用通知方法,在這個方法中則會根據目標角色中注冊的觀察者名單來逐個調用相應的 update 方法來調整觀察者的狀態。這樣觀察者模式就走完了一個流程。
?????? 在下面的例子中會更深刻的體驗到這個流程的。
?
三、 舉例
觀察者模式是我在《 JUnit 源代碼分析》中遺留的一個模式,因此這里將采用 JUnit 來作為例子。
JUnit 為用戶提供了三種不同的測試結果顯示界面,以后還可能會有其它方式的現實界面……。怎么才能將測試的業務邏輯和顯示結果的界面很好的分離開?不用問,就是觀察者模式!
下面我們來看看 JUnit 中觀察者模式的使用代碼:
// 下面是我們的抽象觀察者角色, JUnit 是采用接口來實現的,這也是一般采用的方式。
// 可以看到這里面定義了四個不同的 update 方法,對應四種不同的狀態變化
public interface TestListener {
?????? /**
? ???? ?* An error occurred.
? ???? ?*/
?????? public void addError(Test test, Throwable t);
?????? /**
? ???? ?* A failure occurred.
? ???? ?*/
? ???? public void addFailure(Test test, AssertionFailedError t);?
?????? /**
?????? ?* A test ended.
?????? ?*/
? ???? public void endTest(Test test);
?????? /**
?????? ?* A test started.
?????? ?*/
?????? public void startTest(Test test);
}
?
// 具體觀察者角色,我們采用最簡單的 TextUI 下的情況來說明( AWT , Swing 對于整天做 Web 應用的人來說,已經很陌生了)
public class ResultPrinter implements TestListener {
?????? // 省略好多啊,主要是顯示代碼
……
?????? // 下面就是實現接口 TestListener 的四個方法
?????? // 填充方法的行為很簡單的說
?????? /**
?????? ?* @see junit.framework.TestListener#addError(Test, Throwable)
?????? ?*/
?????? public void addError(Test test, Throwable t) {
????????????? getWriter().print("E");
?????? }
?????? /**
?????? ?* @see junit.framework.TestListener#addFailure(Test, AssertionFailedError)
?????? ?*/
?????? public void addFailure(Test test, AssertionFailedError t) {
????????????? getWriter().print("F");
?????? }
?????? /**
?????? ?* @see junit.framework.TestListener#endTest(Test)
?????? ?*/
?????? public void endTest(Test test) {
?????? }
?????? /**
?????? ?* @see junit.framework.TestListener#startTest(Test)
?????? ?*/
?????? public void startTest(Test test) {
????????????? getWriter().print(".");
????????????? if (fColumn++ >= 40) {
???????????????????? getWriter().println();
???????????????????? fColumn= 0;
????????????? }
?????? }
}
?
來看下我們的目標角色,隨便說下,由于 JUnit 功能的簡單,只有一個目標—— TestResult ,因此 JUnit 只有一個具體目標角色。
// 好長的代碼,好像沒有重點。去掉了大部分與主題無關的信息
// 下面只列出了當 Failures 發生時是怎么來通知觀察者的
public class TestResult extends Object {
?????? // 這個是用來存放測試 Failures 的集合
protected Vector fFailures;
// 這個就是用來存放注冊進來的觀察者的集合
?????? protected Vector fListeners;
?
?????? public TestResult() {
????????????? fFailures= new Vector();
????????????? fListeners= new Vector();
?????? }
?????? /**
?????? ?* Adds a failure to the list of failures. The passed in exception
?????? ?* caused the failure.
?????? ?*/
?????? public synchronized void addFailure(Test test, AssertionFailedError t) {
????????????? fFailures.addElement(new TestFailure(test, t));
????????????? // 下面就是通知各個觀察者的 addFailure 方法
????????????? for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) {
???????????????????? ((TestListener)e.nextElement()).addFailure(test, t);
????????????? }
?????? }
?????? /**
?????? ?* 注冊一個觀察者
?????? ?*/
?????? public synchronized void addListener(TestListener listener) {
????????????? fListeners.addElement(listener);
?????? }
?????? /**
?????? ?* 刪除一個觀察者
?????? ?*/
?????? public synchronized void removeListener(TestListener listener) {
????????????? fListeners.removeElement(listener);
?????? }
?????? /**
?????? ?* 返回一個觀察者集合的拷貝,當然是為了防止對觀察者集合的非法方式操作了
???? * 可以看到所有使用觀察者集合的地方都通過它
?????? ?*/
?????? private synchronized Vector cloneListeners() {
????????????? return (Vector)fListeners.clone();
?????? }
?????? ……
}
?
嗯,觀察者模式組成所需要的角色在這里已經全了。不過好像還是缺點什么……。呵呵,對!就是它們之間還沒有真正的建立聯系。在 JUnit 中是通過 TestRunner 來作的,而你在具體的系統中可以靈活掌握。
看一下 TestRunner 中的代碼:
public class TestRunner extends BaseTestRunner {
?????? private ResultPrinter fPrinter;
public TestResult doRun(Test suite, boolean wait) {
// 就是在這里注冊的
????????????? result.addListener(fPrinter);
……
?
四、 使用情況
GOF 給出了以下使用觀察者模式的情況:
1) ??????? 當一個抽象模型有兩個方面 , 其中一個方面依賴于另一方面。將這二者封裝在獨立的對象中以使它們可以各自獨立地改變和復用。
2) ??????? 當對一個對象的改變需要同時改變其它對象 , 而不知道具體有多少對象有待改變。
3) ??????? 當一個對象必須通知其它對象,而它又不能假定其它對象是誰。換言之 , 你不希望這些對象是緊密耦合的。
其實觀察者模式同前面講過的橋梁、策略有著共同的使用環境:將變化獨立封裝起來,以達到最大的重用和解耦。觀察者與后兩者不同的地方在于,觀察者模式中的目標和觀察者的變化不是獨立的,而是有著某些聯系。
?
五、 我推你拉
觀 察者模式在關于目標角色、觀察者角色通信的具體實現中,有兩個版本。一種情況便是目標角色在發生變化后,僅僅告訴觀察者角色“我變化了”;觀察者角色如果 想要知道具體的變化細節,則就要自己從目標角色的接口中得到。這種模式被很形象的稱為:拉模式——就是說變化的信息是觀察者角色主動從目標角色中“拉”出 來的。
還有一種方法,那就是我目標角色“服務一條龍”,通知你發生變化的同時,通過一個參數將變化的細節傳遞到觀察者角色中去。這就是“推模式”——管你要不要,先給你啦。
這兩種模式的使用,取決于系統設計時的需要。如果目標角色比較復雜,并且觀察者角色進行更新時必須得到一些具體變化的信息,則“推模式”比較合適。如果目標角色比較簡單,則“拉模式”就很合適啦。
?
六、 總結
大概的介紹了下觀察者模式。希望能對你有所幫助。