本文的結構如下:
- 什麼是觀察者模式
- 爲什麼要用該模式
- 模式的結構
- 代碼示例
- 推模型和拉模型
- 優點和缺點
- 適用環境
- 模式應用
- 總結
一、什麼是觀察者模式
觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的所有依賴者都會收到通知並自動更新。觀察者模式又叫做發佈-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。
上面是比較官方的定義,可以用訂閱雜誌的模式來通俗理解觀察者模式。
如上圖,現有一本文學雜誌,一一、二二、三三都喜歡文學,所以花錢訂閱了該雜誌,四四不喜歡文學,在其他三人訂閱雜誌的時候,他選擇躲在窩裏睡大覺。
新的一月到來,雜誌發佈了最新的一期,這天一大早,雜誌社就把雜誌分別送到了一一、二二、三三手中,三人坐着椅子上,一邊喝着熱牛奶,一邊翻看着絕世好文章,四四隻能落寞躲在角落上,偷偷打起農藥。
這就是觀察者模式的通俗解釋,一個“主題”,有一個或多個“觀察者”,“觀察者”訂閱“主題”,“主題”更新,訂閱了該“主題”的“觀察者”都能收到通知,並進行相應的行爲。
二、爲什麼要用該模式
軟件系統常常要求建立一種對象與對象之間的依賴關係,一個對象發生改變時將自動通知其他對象,其他對象將相應做出反應。做到這一點的設計方案有很多,但是爲了使系統能夠易於複用,應該選擇低耦合度的設計方案。減少對象之間的耦合有利於系統的複用,但是同時需要使這些低耦合度的對象之間能夠維持行動的協調一致,保證高度的協作。觀察者模式是滿足這一要求的各種設計方案中最重要的一種。
三、模式的結構
這是摘自《HeadFirst 設計模式》一書中的類圖。從圖中應該能很清楚看出觀察者模式涉及的角色:
- 抽象主題(Subject)角色:抽象主題角色把所有對觀察者對象的引用保存在一個聚集(比如ArrayList對象)裏,每個主題都可以有任何數量的觀察者。抽象主題提供一個接口,可以增加和刪除觀察者對象,抽象主題角色又叫做抽象被觀察者(Observable)角色。
- 具體主題(ConcreteSubject)角色:將有關狀態存入具體觀察者對象;在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知。具體主題角色又叫做具體被觀察者(Concrete Observable)角色。
- 抽象觀察者(Observer)角色:爲所有的具體觀察者定義一個接口,在得到主題的通知時更新自己,這個接口叫做更新接口。
- 具體觀察者(ConcreteObserver)角色:存儲與主題的狀態自恰的狀態。具體觀察者角色實現抽象觀察者角色所要求的更新接口,以便使本身的狀態與主題的狀態相協調。如果需要,具體觀察者角色可以保持一個指向具體主題對象的引用。
四、代碼示例
用代碼實現“訂閱報紙”,觀察者模式運用如下:
1.首先有抽象主題
/**
* 抽象主題
*
* Created by w1992wishes on 2017/10/17.
*/
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
2.有實現抽象主題的具體主題
/**
* 具體主題
*
* Created by w1992wishes on 2017/10/17.
*/
public class Maganize implements Subject {
private List<Observer> observers;
private String flag;
public Maganize(){
observers = new ArrayList<Observer>();
}
public void registerObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
int i = observers.indexOf(observer);
if (i > 0){
observers.remove(i);
}
}
public void notifyObservers() {
for (int i=0; i<observers.size(); i++){
Observer observer = (Observer)observers.get(i);
observer.update(flag);
}
}
public void publishNewMaganize(String flag){
this.flag = flag;
System.out.println("publish new maganize: " + flag);
notifyObservers();
}
}
3.還有抽象觀察者
/**
* 抽象觀察者
*
* Created by w1992wishes on 2017/10/17.
*/
public interface Observer {
void update(String flag);
}
4.還有實現抽象觀察者的三個具體觀察者
/**
* 具體觀察者一一
* Created by w1992wishes on 2017/10/17.
*/
public class Yiyi implements Observer {
private String flag;
public void update(String flag) {
this.flag = flag;
System.out.println("I am yiyi, now reading maganize: " + flag);
}
}
/**
* 具體觀察者--二二
*
* Created by w1992wishes on 2017/10/17.
*/
public class Erer implements Observer {
private String flag;
public void update(String flag) {
this.flag = flag;
System.out.println("I am erer, now reading maganize: " + flag);
}
}
/**
* 具體觀察者三三
*
* Created by w1992wishes on 2017/10/17.
*/
public class Sansan implements Observer {
private String flag;
public void update(String flag) {
this.flag = flag;
System.out.println("I am sansan, now reading maganize: " + flag);
}
}
5.最後有客戶端
/**
* 客戶端
*
* Created by w1992wishes on 2017/10/17.
*/
public class Client {
public static void main(String[] args) {
//創建主題對象
Maganize maganize = new Maganize();
//創建觀察者
Observer yiyi = new Yiyi();
Observer erer = new Erer();
Observer sansan = new Sansan();
//將觀察者對象登記到主題對象上
maganize.registerObserver(yiyi);
maganize.registerObserver(erer);
maganize.registerObserver(sansan);
//改變主題對象的狀態(發佈新雜誌)
maganize.publishNewMaganize("October New");
}
}
顯示結果如下:
publish new maganize: October New
I am yiyi, now reading maganize: October New
I am erer, now reading maganize: October New
I am sansan, now reading maganize: October New
在運行時,這個客戶端首先創建了具體主題類的實例,以及三個觀察者對象。然後,它調用主題對象的registerObserver()方法,將觀察者對象向主題對象登記,也就是將它加入到主題對象的聚集中去。
這時,客戶端調用主題的publishNewMaganize()方法,改變了主題對象的內部狀態。主題對象在狀態發生變化時,調用超類的notifyObservers()方法,通知所有登記過的觀察者對象。
五、推模型和拉模型
6.1、推模型
在觀察者模式中,又分爲推模型和拉模型兩種方式。
- 推模型
主題對象向觀察者推送主題的詳細信息,不管觀察者是否需要,推送的信息通常是主題對象的全部或部分數據。
- 拉模型
主題對象在通知觀察者的時候,只傳遞少量信息。如果觀察者需要更具體的信息,由觀察者主動到主題對象中獲取,相當於是觀察者從主題對象中拉數據。一般這種模型的實現中,會把主題對象自身通過update()方法傳遞給觀察者,這樣在觀察者需要獲取數據的時候,就可以通過這個引用來獲取了。
根據上面的描述,發現前面的例子就是典型的推模型,下面給出一個拉模型的實例。
6.2、拉模型
1.拉模型通常都是把主題對象當做參數傳遞。
/**
* 拉模型 抽象觀察者
*
* Created by w1992wishes on 2017/10/17.
*/
public interface Observer {
/**
* update 方法
*
* @param subject 傳入主題對象,方面獲取相應的主題對象的狀態
*/
void update(Subject subject);
}
2.拉模型的具體觀察者類
/**
* 拉模型 具體觀察者一一
* Created by w1992wishes on 2017/10/17.
*/
public class Yiyi implements Observer {
private String flag;
public void update(Subject subject) {
this.flag = ((Maganize) subject).getFlag();
System.out.println("I am yiyi, now reading maganize: " + flag);
}
}
/**
* 拉模型 具體觀察者--二二
*
* Created by w1992wishes on 2017/10/17.
*/
public class Erer implements Observer {
private String flag;
public void update(Subject subject) {
this.flag = ((Maganize) subject).getFlag();
System.out.println("I am erer, now reading maganize: " + flag);
}
}
/**
* 拉模型 具體觀察者三三
*
* Created by w1992wishes on 2017/10/17.
*/
public class Sansan implements Observer {
private String flag;
public void update(Subject subject) {
this.flag = ((Maganize) subject).getFlag();
System.out.println("I am sansan, now reading maganize: " + flag);
}
}
3.拉模型 拉模型的具體主題類
拉模型的主題類主要的改變是nodifyObservers()方法。在循環通知觀察者的時候,也就是循環調用觀察者的update()方法的時候,傳入的參數不同了。
/**
* 具體主題
*
* Created by w1992wishes on 2017/10/17.
*/
public class Maganize implements Subject {
private List<Observer> observers;
private String flag;
public Maganize(){
observers = new ArrayList<Observer>();
}
public void registerObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
int i = observers.indexOf(observer);
if (i > 0){
observers.remove(i);
}
}
public void notifyObservers() {
for (int i=0; i<observers.size(); i++){
Observer observer = (Observer)observers.get(i);
observer.update(this);
}
}
public void publishNewMaganize(String flag){
this.flag = flag;
System.out.println("publish new maganize: " + flag);
notifyObservers();
}
public String getFlag() {
return flag;
}
}
6.3、兩種模式的比較
- 推模型是假定主題對象知道觀察者需要的數據;而拉模型是主題對象不知道觀察者具體需要什麼數據,沒有辦法的情況下,乾脆把自身傳遞給觀察者,讓觀察者自己去按需要取值。
- 推模型可能會使得觀察者對象難以複用,因爲觀察者的update()方法是按需要定義的參數,可能無法兼顧沒有考慮到的使用情況。這就意味着出現新情況的時候,就可能提供新的update()方法,或者是乾脆重新實現觀察者;而拉模型就不會造成這樣的情況,因爲拉模型下,update()方法的參數是主題對象本身,這基本上是主題對象能傳遞的最大數據集合了,基本上可以適應各種情況的需要。
六、優點和缺點
6.1、優點
- 觀察者模式可以實現表示層和數據邏輯層的分離,並定義了穩定的消息更新傳遞機制,抽象了更新接口,使得可以有各種各樣不同的表示層作爲具體觀察者角色。
- 觀察者模式在觀察目標和觀察者之間建立一個抽象的耦合。
- 觀察者模式支持廣播通信。
- 觀察者模式符合“開閉原則”的要求。
6.2、缺點
- 如果一個觀察目標對象有很多直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。
- 如果在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間進行循環調用,可能導致系統崩潰。
- 觀察者模式沒有相應的機制讓觀察者知道所觀察的目標對象是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
七、適用環境
在以下情況下可以使用觀察者模式:
- 一個抽象模型有兩個方面,其中一個方面依賴於另一個方面。將這些方面封裝在獨立的對象中使它們可以各自獨立地改變和複用。
- 一個對象的改變將導致其他一個或多個對象也發生改變,而不知道具體有多少對象將發生改變,可以降低對象之間的耦合度。
- 一個對象必須通知其他對象,而並不知道這些對象是誰。
- 需要在系統中創建一個觸發鏈,A對象的行爲將影響B對象,B對象的行爲將影響C對象……,可以使用觀察者模式創建一種鏈式觸發機制。
八、模式應用
觀察者模式在軟件開發中應用非常廣泛,如某電子商務網站可以在執行發送操作後給用戶多個發送商品打折信息,某團隊戰鬥遊戲中某隊友犧牲將給所有成員提示等等,凡是涉及到一對一或者一對多的對象交互場景都可以使用觀察者模式。
九、總結
- 觀察者模式定義對象間的一種一對多依賴關係,使得每當一個對象狀態發生改變時,其相關依賴對象皆得到通知並被自動更新。觀察者模式又叫做發佈-訂閱模式、模型-視圖模式、源-監聽器模式或從屬者模式。觀察者模式是一種對象行爲型模式。
- 觀察者模式包含四個角色:目標又稱爲主題,它是指被觀察的對象;具體目標是目標類的子類,通常它包含有經常發生改變的數據,當它的狀態發生改變時,向它的各個觀察者發出通知;觀察者將對觀察目標的改變做出反應;在具體觀察者中維護一個指向具體目標對象的引用,它存儲具體觀察者的有關狀態,這些狀態需要和具體目標的狀態保持一致。
- 觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個目標對象,當這個目標對象的狀態發生變化時,會通知所有觀察者對象,使它們能夠自動更新。
- 觀察者模式的主要優點在於可以實現表示層和數據邏輯層的分離,並在觀察目標和觀察者之間建立一個抽象的耦合,支持廣播通信;其主要缺點在於如果一個觀察目標對象有很多直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間,而且如果在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間進行循環調用,可能導致系統崩潰。
- 觀察者模式適用情況包括:一個抽象模型有兩個方面,其中一個方面依賴於另一個方面;一個對象的改變將導致其他一個或多個對象也發生改變,而不知道具體有多少對象將發生改變;一個對象必須通知其他對象,而並不知道這些對象是誰;需要在系統中創建一個觸發鏈。
- 在JDK的java.util包中,提供了Observable類以及Observer接口,它們構成了Java語言對觀察者模式的支持。