對象間的聯動——觀察者模式

轉:http://blog.csdn.net/lovelion/article/details/7720232


 “紅燈停,綠燈行”,在日常生活中,交通信號燈裝點着我們的城市,指揮着日益擁擠的城市交通。當紅燈亮起,來往的汽車將停止;而綠燈亮起,汽車可以繼續前行。在這個過程中,交通信號燈是汽車(更準確地說應該是汽車駕駛員)的觀察目標,而汽車是觀察者。隨着交通信號燈的變化,汽車的行爲也將隨之而變化,一盞交通信號燈可以指揮多輛汽車。如圖22-1所示:

22-1  交通信號燈與汽車示意圖

      在軟件系統中,有些對象之間也存在類似交通信號燈和汽車之間的關係,一個對象的狀態或行爲的變化將導致其他對象的狀態或行爲也發生改變,它們之間將產生聯動,正所謂“觸一而牽百發”。爲了更好地描述對象之間存在的這種一對多(包括一對一)的聯動,觀察者模式應運而生,它定義了對象之間一種一對多的依賴關係,讓一個對象的改變能夠影響其他對象。本章我們將學習用於實現對象間聯動的觀察者模式。


1  多人聯機對戰遊戲的設計

      Sunny軟件公司欲開發一款多人聯機對戰遊戲(類似魔獸世界、星際爭霸等遊戲),在該遊戲中,多個玩家可以加入同一戰隊組成聯盟,當戰隊中某一成員受到敵人攻擊時將給所有其他盟友發送通知,盟友收到通知後將作出響應。

      Sunny軟件公司開發人員需要提供一個設計方案來實現戰隊成員之間的聯動。

       Sunny軟件公司開發人員通過對系統功能需求進行分析,發現在該系統中戰隊成員之間的聯動過程可以簡單描述如下:

      聯盟成員受到攻擊-->發送通知給盟友-->盟友作出響應

      如果按照上述思路來設計系統,由於聯盟成員在受到攻擊時需要通知他的每一個盟友,因此每個聯盟成員都需要持有其他所有盟友的信息,這將導致系統開銷較大,因此Sunny公司開發人員決定引入一個新的角色——“戰隊控制中心”——來負責維護和管理每個戰隊所有成員的信息。當一個聯盟成員受到攻擊時,將向相應的戰隊控制中心發送求助信息,戰隊控制中心再逐一通知每個盟友,盟友再作出響應,如圖22-2所示:

22-2   多人聯機對戰遊戲中對象的聯動

       在圖22-2中,受攻擊的聯盟成員將與戰隊控制中心產生聯動,戰隊控制中心還將與其他盟友產生聯動。

       如何實現對象之間的聯動?如何讓一個對象的狀態或行爲改變時,依賴於它的對象能夠得到通知並進行相應的處理?

       彆着急,本章所介紹的觀察者模式將爲對象之間的聯動提供一個優秀的解決方案,下面就讓我們正式進入觀察者模式的學習。


2  觀察者模式概述

      觀察者模式是使用頻率最高的設計模式之一,它用於建立一種對象與對象之間的依賴關係,一個對象發生改變時將自動通知其他對象,其他對象將相應作出反應。在觀察者模式中,發生改變的對象稱爲觀察目標,而被通知的對象稱爲觀察者,一個觀察目標可以對應多個觀察者,而且這些觀察者之間可以沒有任何相互聯繫,可以根據需要增加和刪除觀察者,使得系統更易於擴展。

      觀察者模式定義如下:

觀察者模式(Observer Pattern):定義對象之間的一種一對多依賴關係,使得每當一個對象狀態發生改變時,其相關依賴對象皆得到通知並被自動更新。觀察者模式的別名包括髮布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。觀察者模式是一種對象行爲型模式。

      觀察者模式結構中通常包括觀察目標和觀察者兩個繼承層次結構,其結構如圖22-3所示:

22-3  觀察者模式結構圖

      在觀察者模式結構圖中包含如下幾個角色:

      ● Subject(目標):目標又稱爲主題,它是指被觀察的對象。在目標中定義了一個觀察者集合,一個觀察目標可以接受任意數量的觀察者來觀察,它提供一系列方法來增加和刪除觀察者對象,同時它定義了通知方法notify()。目標類可以是接口,也可以是抽象類或具體類。

      ● ConcreteSubject(具體目標):具體目標是目標類的子類,通常它包含有經常發生改變的數據,當它的狀態發生改變時,向它的各個觀察者發出通知;同時它還實現了在目標類中定義的抽象業務邏輯方法(如果有的話)。如果無須擴展目標類,則具體目標類可以省略。

      ● Observer(觀察者):觀察者將對觀察目標的改變做出反應,觀察者一般定義爲接口,該接口聲明瞭更新數據的方法update(),因此又稱爲抽象觀察者。

      ● ConcreteObserver(具體觀察者):在具體觀察者中維護一個指向具體目標對象的引用,它存儲具體觀察者的有關狀態,這些狀態需要和具體目標的狀態保持一致;它實現了在抽象觀察者Observer中定義的update()方法。通常在實現時,可以調用具體目標類的attach()方法將自己添加到目標類的集合中或通過detach()方法將自己從目標類的集合中刪除。

      觀察者模式描述瞭如何建立對象與對象之間的依賴關係,以及如何構造滿足這種需求的系統。觀察者模式包含觀察目標和觀察者兩類對象,一個目標可以有任意數目的與之相依賴的觀察者,一旦觀察目標的狀態發生改變,所有的觀察者都將得到通知。作爲對這個通知的響應,每個觀察者都將監視觀察目標的狀態以使其狀態與目標狀態同步,這種交互也稱爲發佈-訂閱(Publish-Subscribe)。觀察目標是通知的發佈者,它發出通知時並不需要知道誰是它的觀察者,可以有任意數目的觀察者訂閱它並接收通知。

      下面通過示意代碼來對該模式進行進一步分析。首先我們定義一個抽象目標Subject,典型代碼如下所示:

import java.util.*;
abstract class Subject {
    //定義一個觀察者集合用於存儲所有觀察者對象
protected ArrayList observers<Observer> = new ArrayList();

//註冊方法,用於向觀察者集合中增加一個觀察者
	public void attach(Observer observer) {
    observers.add(observer);
}

    //註銷方法,用於在觀察者集合中刪除一個觀察者
	public void detach(Observer observer) {
    observers.remove(observer);
}

    //聲明抽象通知方法
	public abstract void notify();
}
具體目標類ConcreteSubject是實現了抽象目標類Subject的一個具體子類,其典型代碼如下所示:
class ConcreteSubject extends Subject {
    //實現通知方法
	public void notify() {
        //遍歷觀察者集合,調用每一個觀察者的響應方法
		for(Object obs:observers) {
			((Observer)obs).update();
		}
	}	
}
  抽象觀察者角色一般定義爲一個接口,通常只聲明一個update()方法,爲不同觀察者的更新(響應)行爲定義相同的接口,這個方法在其子類中實現,不同的觀察者具有不同的響應方法。抽象觀察者Observer典型代碼如下所示:
interface Observer {
    //聲明響應方法
	public void update();
}
在具體觀察者ConcreteObserver中實現了update()方法,其典型代碼如下所示:
class ConcreteObserver implements Observer {
    //實現響應方法
	public void update() {
		//具體響應代碼
	}
}

  在有些更加複雜的情況下,具體觀察者類ConcreteObserverupdate()方法在執行時需要使用到具體目標類ConcreteSubject中的狀態(屬性),因此在ConcreteObserverConcreteSubject之間有時候還存在關聯或依賴關係,在ConcreteObserver中定義一個ConcreteSubject實例,通過該實例獲取存儲在ConcreteSubject中的狀態。如果ConcreteObserverupdate()方法不需要使用到ConcreteSubject中的狀態屬性,則可以對觀察者模式的標準結構進行簡化,在具體觀察者ConcreteObserver和具體目標ConcreteSubject之間無須維持對象引用。如果在具體層具有關聯關係,系統的擴展性將受到一定的影響,增加新的具體目標類有時候需要修改原有觀察者的代碼,在一定程度上違反了“開閉原則”,但是如果原有觀察者類無須關聯新增的具體目標,則系統擴展性不受影響。

 

思考

觀察者模式是否符合“開閉原則”?【從增加具體觀察者和增加具體目標類兩方面考慮。】


3 完整解決方案

      爲了實現對象之間的聯動,Sunny軟件公司開發人員決定使用觀察者模式來進行多人聯機對戰遊戲的設計,其基本結構如圖22-4所示:

22-4  多人聯機對戰遊戲結構圖

      在圖22-4中,AllyControlCenter充當目標類,ConcreteAllyControlCenter充當具體目標類,Observer充當抽象觀察者,Player充當具體觀察者。完整代碼如下所示:

import java.util.*;

//抽象觀察類
interface Observer {
	public String getName();
	public void setName(String name);
	public void help(); //聲明支援盟友方法
	public void beAttacked(AllyControlCenter acc); //聲明遭受攻擊方法
}

//戰隊成員類:具體觀察者類
class Player implements Observer {
	private String name;

	public Player(String name) {
		this.name = name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public String getName() {
		return this.name;
	}
	
    //支援盟友方法的實現
	public void help() {
		System.out.println("堅持住," + this.name + "來救你!");
	}
	
    //遭受攻擊方法的實現,當遭受攻擊時將調用戰隊控制中心類的通知方法notifyObserver()來通知盟友
	public void beAttacked(AllyControlCenter acc) {
        System.out.println(this.name + "被攻擊!");
        acc.notifyObserver(name);		
	}
}

//戰隊控制中心類:目標類
abstract class AllyControlCenter {
	protected String allyName; //戰隊名稱
	protected ArrayList<Observer> players = new ArrayList<Observer>(); //定義一個集合用於存儲戰隊成員
	
	public void setAllyName(String allyName) {
		this.allyName = allyName;
	}
	
	public String getAllyName() {
		return this.allyName;
	}
	
    //註冊方法
	public void join(Observer obs) {
		System.out.println(obs.getName() + "加入" + this.allyName + "戰隊!");
		players.add(obs);
	}
	
    //註銷方法
	public void quit(Observer obs) {
		System.out.println(obs.getName() + "退出" + this.allyName + "戰隊!");
		players.remove(obs);
	}
	
    //聲明抽象通知方法
	public abstract void notifyObserver(String name);
}

//具體戰隊控制中心類:具體目標類
class ConcreteAllyControlCenter extends AllyControlCenter {
	public ConcreteAllyControlCenter(String allyName) {
		System.out.println(allyName + "戰隊組建成功!");
		System.out.println("----------------------------");
		this.allyName = allyName;
	}
	
    //實現通知方法
	public void notifyObserver(String name) {
		System.out.println(this.allyName + "戰隊緊急通知,盟友" + name + "遭受敵人攻擊!");
        //遍歷觀察者集合,調用每一個盟友(自己除外)的支援方法
        for(Object obs : players) {
            if (!((Observer)obs).getName().equalsIgnoreCase(name)) {
                ((Observer)obs).help();
            }
        }		
	}
}

編寫如下客戶端測試代碼:
class Client {
	public static void main(String args[]) {
		//定義觀察目標對象
AllyControlCenter acc;
		acc = new ConcreteAllyControlCenter("金庸羣俠");
		
        //定義四個觀察者對象
		Observer player1,player2,player3,player4;
		
		player1 = new Player("楊過");
		acc.join(player1);
		
		player2 = new Player("令狐沖");
		acc.join(player2);
		
		player3 = new Player("張無忌");
		acc.join(player3);
		
		player4 = new Player("段譽");
		acc.join(player4);
		
		//某成員遭受攻擊
		Player1.beAttacked(acc);
	}
}

編譯並運行程序,輸出結果如下:

金庸羣俠戰隊組建成功!

----------------------------

楊過加入金庸羣俠戰隊!

令狐沖加入金庸羣俠戰隊!

張無忌加入金庸羣俠戰隊!

段譽加入金庸羣俠戰隊!

楊過被攻擊!

金庸羣俠戰隊緊急通知,盟友楊過遭受敵人攻擊!

堅持住,令狐沖來救你!

堅持住,張無忌來救你!

堅持住,段譽來救你!

      在本實例中,實現了兩次對象之間的聯動,當一個遊戲玩家Player對象的beAttacked()方法被調用時,將調用AllyControlCenternotifyObserver()方法來進行處理,而在notifyObserver()方法中又將調用其他Player對象的help()方法。PlayerbeAttacked()方法、AllyControlCenternotifyObserver()方法以及Playerhelp()方法構成了一個聯動觸發鏈,執行順序如下所示:

Player.beAttacked() --> AllyControlCenter.notifyObserver() -->Player.help()


4 JDK對觀察者模式的支持

      觀察者模式在Java語言中的地位非常重要。在JDKjava.util包中,提供了Observable類以及Observer接口,它們構成了JDK對觀察者模式的支持。如圖22-5所示:

22-5 JDK提供的Observable類及Observer接口結構圖

      (1)  Observer接口

      在java.util.Observer接口中只聲明一個方法,它充當抽象觀察者,其方法聲明代碼如下所示:

void  update(Observable o, Object arg);

      當觀察目標的狀態發生變化時,該方法將會被調用,在Observer的子類中將實現update()方法,即具體觀察者可以根據需要具有不同的更新行爲。當調用觀察目標類ObservablenotifyObservers()方法時,將執行觀察者類中的update()方法。

      (2)  Observable

      java.util.Observable類充當觀察目標類,在Observable中定義了一個向量Vector來存儲觀察者對象,它所包含的方法及說明見表22-1

22-1 Observable類所包含方法及說明

方法名

方法描述

Observable()

構造方法,實例化Vector向量。

addObserver(Observer  o)

用於註冊新的觀察者對象到向量中。

deleteObserver  (Observer o)

用於刪除向量中的某一個觀察者對象。

notifyObservers()notifyObservers(Object arg)

通知方法,用於在方法內部循環調用向量中每一個觀察者的update()方法。

deleteObservers()

用於清空向量,即刪除向量中所有觀察者對象。

setChanged()

該方法被調用後會設置一個boolean類型的內部標記變量changed的值爲true,表示觀察目標對象的狀態發生了變化。

clearChanged()

用於將changed變量的值設爲false,表示對象狀態不再發生改變或者已經通知了所有的觀察者對象,調用了它們的update()方法。

hasChanged()

用於測試對象狀態是否改變。

countObservers()

用於返回向量中觀察者的數量。

      我們可以直接使用Observer接口和Observable類來作爲觀察者模式的抽象層,再自定義具體觀察者類和具體觀察目標類,通過使用JDK中的Observer接口和Observable類,可以更加方便地在Java語言中應用觀察者模式。


5 觀察者模式與Java事件處理

       JDK 1.0及更早版本的事件模型基於職責鏈模式,但是這種模型不適用於複雜的系統,因此在JDK 1.1及以後的各個版本中,事件處理模型採用基於觀察者模式的委派事件模型(DelegationEvent Model, DEM),即一個Java組件所引發的事件並不由引發事件的對象自己來負責處理,而是委派給獨立的事件處理對象負責

      在DEM模型中,目標角色(如界面組件)負責發佈事件,而觀察者角色(事件處理者)可以向目標訂閱它所感興趣的事件。當一個具體目標產生一個事件時,它將通知所有訂閱者。事件的發佈者稱爲事件源(Event Source),而訂閱者稱爲事件監聽器(Event Listener),在這個過程中還可以通過事件對象(Event Object)來傳遞與事件相關的信息,可以在事件監聽者的實現類中實現事件處理,因此事件監聽對象又可以稱爲事件處理對象。事件源對象、事件監聽對象(事件處理對象)和事件對象構成了Java事件處理模型的三要素。事件源對象充當觀察目標,而事件監聽對象充當觀察者。以按鈕點擊事件爲例,其事件處理流程如下:

       (1) 如果用戶在GUI中單擊一個按鈕,將觸發一個事件(如ActionEvent類型的動作事件),JVM將產生一個相應的ActionEvent類型的事件對象,在該事件對象中包含了有關事件和事件源的信息,此時按鈕是事件源對象;

       (2) ActionEvent事件對象傳遞給事件監聽對象(事件處理對象),JDK提供了專門用於處理ActionEvent事件的接口ActionListener,開發人員需提供一個ActionListener的實現類(如MyActionHandler),實現在ActionListener接口中聲明的抽象事件處理方法actionPerformed(),對所發生事件做出相應的處理;

       (3) 開發人員將ActionListener接口的實現類(如MyActionHandler)對象註冊到按鈕中,可以通過按鈕類的addActionListener()方法來實現註冊;

       (4) JVM在觸發事件時將調用按鈕的fireXXX()方法,在該方法內部將調用註冊到按鈕中的事件處理對象的actionPerformed()方法,實現對事件的處理。

       使用類似的方法,我們可自定義GUI組件,如包含兩個文本框和兩個按鈕的登錄組件LoginBean,可以採用如圖22-6所示設計方案:

22-6 自定義登錄組件結構圖【省略按鈕、文本框等界面組件】

       圖22-6中相關類說明如下:

       (1) LoginEvent是事件類,它用於封裝與事件有關的信息,它不是觀察者模式的一部分,但是它可以在目標對象和觀察者對象之間傳遞數據,在AWT事件模型中,所有的自定義事件類都是java.util.EventObject的子類。

       (2) LoginEventListener充當抽象觀察者,它聲明瞭事件響應方法validateLogin(),用於處理事件,該方法也稱爲事件處理方法,validateLogin()方法將一個LoginEvent類型的事件對象作爲參數,用於傳輸與事件相關的數據,在其子類中實現該方法,實現具體的事件處理。

       (3) LoginBean充當具體目標類,在這裏我們沒有定義抽象目標類,對觀察者模式進行了一定的簡化。在LoginBean中定義了抽象觀察者LoginEventListener類型的對象lel和事件對象LoginEvent,提供了註冊方法addLoginEventListener()用於添加觀察者,Java事件處理中,通常使用的是一對一的觀察者模式,而不是一對多的觀察者模式,也就是說,一個觀察目標中只定義一個觀察者對象,而不是提供一個觀察者對象的集合。在LoginBean中還定義了通知方法fireLoginEvent(),該方法在Java事件處理模型中稱爲“點火方法”,在該方法內部實例化了一個事件對象LoginEvent,將用戶輸入的信息傳給觀察者對象,並且調用了觀察者對象的響應方法validateLogin()

      (4) LoginValidatorALoginValidatorB充當具體觀察者類,它們實現了在LoginEventListener接口中聲明的抽象方法validateLogin(),用於具體實現事件處理,該方法包含一個LoginEvent類型的參數,在LoginValidatorALoginValidatorB類中可以針對相同的事件提供不同的實現。


6 觀察者模式與MVC

      在當前流行的MVC(Model-View-Controller)架構中也應用了觀察者模式,MVC是一種架構模式,它包含三個角色:模型(Model),視圖(View)和控制器(Controller)。其中模型可對應於觀察者模式中的觀察目標,而視圖對應於觀察者,控制器可充當兩者之間的中介者。當模型層的數據發生改變時,視圖層將自動改變其顯示內容。如圖22-7所示:

22-7 MVC結構示意圖

      在圖22-7中,模型層提供的數據是視圖層所觀察的對象,在視圖層中包含兩個用於顯示數據的圖表對象,一個是柱狀圖,一個是餅狀圖,相同的數據擁有不同的圖表顯示方式,如果模型層的數據發生改變,兩個圖表對象將隨之發生變化,這意味着圖表對象依賴模型層提供的數據對象,因此數據對象的任何狀態改變都應立即通知它們。同時,這兩個圖表之間相互獨立,不存在任何聯繫,而且圖表對象的個數沒有任何限制,用戶可以根據需要再增加新的圖表對象,如折線圖。在增加新的圖表對象時,無須修改原有類庫,滿足“開閉原則”。

擴展

大家可以查閱相關資料對MVC模式進行深入學習,如Oracle公司提供的技術文檔《Java SE Application Design With MVC》,參考鏈接:http://www.oracle.com/technetwork/articles/javase/index-142890.html


7 觀察者模式總結

      觀察者模式是一種使用頻率非常高的設計模式,無論是移動應用、Web應用或者桌面應用,觀察者模式幾乎無處不在,它爲實現對象之間的聯動提供了一套完整的解決方案,凡是涉及到一對一或者一對多的對象交互場景都可以使用觀察者模式。觀察者模式廣泛應用於各種編程語言的GUI事件處理的實現,在基於事件的XML解析技術(如SAX2)以及Web事件處理中也都使用了觀察者模式。

      1.主要優點

      觀察者模式的主要優點如下:

      (1) 觀察者模式可以實現表示層和數據邏輯層的分離,定義了穩定的消息更新傳遞機制,並抽象了更新接口,使得可以有各種各樣不同的表示層充當具體觀察者角色。

      (2) 觀察者模式在觀察目標和觀察者之間建立一個抽象的耦合。觀察目標只需要維持一個抽象觀察者的集合,無須瞭解其具體觀察者。由於觀察目標和觀察者沒有緊密地耦合在一起,因此它們可以屬於不同的抽象化層次。

      (3) 觀察者模式支持廣播通信,觀察目標會向所有已註冊的觀察者對象發送通知,簡化了一對多系統設計的難度。

      (4) 觀察者模式滿足“開閉原則”的要求,增加新的具體觀察者無須修改原有系統代碼,在具體觀察者與觀察目標之間不存在關聯關係的情況下,增加新的觀察目標也很方便。

      2.主要缺點

      觀察者模式的主要缺點如下:

      (1) 如果一個觀察目標對象有很多直接和間接觀察者,將所有的觀察者都通知到會花費很多時間。

      (2) 如果在觀察者和觀察目標之間存在循環依賴,觀察目標會觸發它們之間進行循環調用,可能導致系統崩潰。

      (3) 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。

      3.適用場景

      在以下情況下可以考慮使用觀察者模式:

      (1) 一個抽象模型有兩個方面,其中一個方面依賴於另一個方面,將這兩個方面封裝在獨立的對象中使它們可以各自獨立地改變和複用。

      (2) 一個對象的改變將導致一個或多個其他對象也發生改變,而並不知道具體有多少對象將發生改變,也不知道這些對象是誰。

      (3) 需要在系統中創建一個觸發鏈,A對象的行爲將影響B對象,B對象的行爲將影響C對象……,可以使用觀察者模式創建一種鏈式觸發機制。

練習

Sunny軟件公司欲開發一款實時在線股票軟件,該軟件需提供如下功能:當股票購買者所購買的某支股票價格變化幅度達到5%時,系統將自動發送通知(包括新價格)給購買該股票的所有股民。試使用觀察者模式設計並實現該系統。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章