1 場景問題#
1.1 訂閱報紙的過程##
來考慮實際生活中訂閱報紙的過程,這里簡單總結(jié)了一下,訂閱報紙的基本流程如下:
首先按照自己的需要選擇合適的報紙,具體的報刊雜志目錄可以從郵局獲取;
選擇好后,就到郵局去填寫訂閱單,同時交上所需的費用;
至此,就完成了報紙的訂閱過程,接下去的就是耐心等候,報社會按照出報時間推出報紙,然后報紙會被送到每個訂閱人的手里。
畫個圖來描述上述過程,如圖所示:
雖然看起來訂閱者是直接跟郵局在打交道,但實際上,訂閱者的訂閱數(shù)據(jù)是會被郵局傳遞到報社的,當(dāng)報社出版了報紙,報社會按照訂閱信息把報紙交給郵局,然后由郵局來代為發(fā)送到訂閱者的手中。所以在整個過程中,郵局只不過起到一個中轉(zhuǎn)的作用,為了簡單,我們?nèi)サ羿]局,讓訂閱者直接和報社交互,如圖所示:
1.2 訂閱報紙的問題##
在上述過程中,訂閱者在完成訂閱后,最關(guān)心的問題就是何時能收到新出的報紙。幸好在現(xiàn)實生活中,報紙都是定期出版,這樣發(fā)放到訂閱者手中也基本上有一個大致的時間范圍,差不多到時間了,訂閱者就會看看郵箱,查收新的報紙。
要是報紙出版的時間不固定呢?
那訂閱者就麻煩了,如果訂閱者想要第一時間閱讀到新報紙,恐怕只能天天守著郵箱了,這未免也太痛苦了吧。
繼續(xù)引申一下,用類來描述上述的過程,描述如下:
訂閱者類向出版者類訂閱報紙,很明顯不會只有一個訂閱者訂閱報紙,訂閱者類可以有很多;當(dāng)出版者類出版新報紙的時候,多個訂閱者類如何知道呢?還有訂閱者類如何得到新報紙的內(nèi)容呢?
把上面的問題對比描述一下:
進(jìn)一步抽象描述這個問題:當(dāng)一個對象的狀態(tài)發(fā)生改變的時候,如何讓依賴于它的所有對象得到通知,并進(jìn)行相應(yīng)的處理呢?
該如何解決這樣的問題?
2 解決方案#
2.1 觀察者模式來解決##
用來解決上述問題的一個合理的解決方案就是觀察者模式。那么什么是觀察者模式呢?
- 觀察者模式定義
- 應(yīng)用觀察者模式來解決的思路
在前面描述的訂閱報紙的例子里面,對于報社來說,在一開始,它并不清楚究竟有多少個訂閱者會來訂閱報紙,因此,報社需要維護(hù)一個訂閱者的列表,這樣當(dāng)報社出版報紙的時候,才能夠把報紙發(fā)放到所有的訂閱者手中。對于訂閱者來說,訂閱者也就是看報的讀者,多個訂閱者會訂閱同一份報紙。
這就出現(xiàn)了一個典型的一對多的對象關(guān)系,一個報紙對象,會有多個訂閱者對象來訂閱;當(dāng)報紙出版的時候,也就是報紙對象改變的時候,需要通知所有的訂閱者對象。那么怎么來建立并維護(hù)這樣的關(guān)系呢?
觀察者模式可以處理這種問題,觀察者模式把這多個訂閱者稱為觀察者:Observer,多個觀察者觀察的對象被稱為目標(biāo):Subject。
一個目標(biāo)可以有任意多個觀察者對象,一旦目標(biāo)的狀態(tài)發(fā)生了改變,所有注冊的觀察者都會得到通知,然后各個觀察者會對通知作出相應(yīng)的響應(yīng),執(zhí)行相應(yīng)的業(yè)務(wù)功能處理,并使自己的狀態(tài)和目標(biāo)對象的狀態(tài)保持一致。
2.2 模式結(jié)構(gòu)和說明##
觀察者模式結(jié)構(gòu)如圖所示:
Subject:目標(biāo)對象,通常具有如下功能:
1. 一個目標(biāo)可以被多個觀察者觀察; 2. 目標(biāo)提供對觀察者注冊和退訂的維護(hù); 3. 當(dāng)目標(biāo)的狀態(tài)發(fā)生變化時,目標(biāo)負(fù)責(zé)通知所有注冊的、有效的觀察者;
Observer:定義觀察者的接口,提供目標(biāo)通知時對應(yīng)的更新方法,這個更新方法進(jìn)行相應(yīng)的業(yè)務(wù)處理,可以在這個方法里面回調(diào)目標(biāo)對象,以獲取目標(biāo)對象的數(shù)據(jù)。
ConcreteSubject:具體的目標(biāo)實現(xiàn)對象,用來維護(hù)目標(biāo)狀態(tài),當(dāng)目標(biāo)對象的狀態(tài)發(fā)生改變時,通知所有注冊有效的觀察者,讓觀察者執(zhí)行相應(yīng)的處理。
ConcreteObserver:觀察者的具體實現(xiàn)對象,用來接收目標(biāo)的通知,并進(jìn)行相應(yīng)的后續(xù)處理,比如更新自身的狀態(tài)以保持和目標(biāo)的相應(yīng)狀態(tài)一致。
2.3 觀察者模式示例代碼##
- 先來看看目標(biāo)對象的定義,示例代碼如下:
/**
* 目標(biāo)對象,它知道觀察它的觀察者,并提供注冊和刪除觀察者的接口
*/
public class Subject {
/**
* 用來保存注冊的觀察者對象
*/
private List<Observer> observers = new ArrayList<Observer>();
/**
* 注冊觀察者對象
* @param observer 觀察者對象
*/
public void attach(Observer observer) {
observers.add(observer);
}
/**
* 刪除觀察者對象
* @param observer 觀察者對象
*/
public void detach(Observer observer) {
observers.remove(observer);
}
/**
* 通知所有注冊的觀察者對象
*/
protected void notifyObservers() {
for(Observer observer : observers){
observer.update(this);
}
}
}
- 接下來看看具體的目標(biāo)對象,示例代碼如下:
/**
* 具體的目標(biāo)對象,負(fù)責(zé)把有關(guān)狀態(tài)存入到相應(yīng)的觀察者對象,
* 并在自己狀態(tài)發(fā)生改變時,通知各個觀察者
*/
public class ConcreteSubject extends Subject {
/**
* 示意,目標(biāo)對象的狀態(tài)
*/
private String subjectState;
public String getSubjectState() {
return subjectState;
}
public void setSubjectState(String subjectState) {
this.subjectState = subjectState;
//狀態(tài)發(fā)生了改變,通知各個觀察者
this.notifyObservers();
}
}
- 再來看看觀察者的接口定義,示例代碼如下:
/**
* 觀察者接口,定義一個更新的接口給那些在目標(biāo)發(fā)生改變的時候被通知的對象
*/
public interface Observer {
/**
* 更新的接口
* @param subject 傳入目標(biāo)對象,好獲取相應(yīng)的目標(biāo)對象的狀態(tài)
*/
public void update(Subject subject);
}
- 接下來看看觀察者的具體實現(xiàn)示意,示例代碼如下:
/**
* 具體觀察者對象,實現(xiàn)更新的方法,使自身的狀態(tài)和目標(biāo)的狀態(tài)保持一致
*/
public class ConcreteObserver implements Observer {
/**
* 示意,觀者者的狀態(tài)
*/
private String observerState;
public void update(Subject subject) {
// 具體的更新實現(xiàn)
//這里可能需要更新觀察者的狀態(tài),使其與目標(biāo)的狀態(tài)保持一致
observerState = ((ConcreteSubject)subject).getSubjectState();
}
}
2.4 使用觀察者模式實現(xiàn)示例##
要使用觀察者模式來實現(xiàn)示例,那就按照前面講述的實現(xiàn)思路,把報紙對象當(dāng)作目標(biāo),然后訂閱者當(dāng)做觀察者,就可以實現(xiàn)出來了。
使用觀察者模式來實現(xiàn)示例的結(jié)構(gòu)如圖所示:
- 被觀察的目標(biāo)
在前面描述的訂閱報紙的例子里面,多個訂閱者都是在觀察同一個報社對象,這個報社對象就是被觀察的目標(biāo)。這個目標(biāo)的接口應(yīng)該有些什么方法呢?還是從實際入手去想,看看報社都有些什么功能。報社最基本有如下的功能:
注冊訂閱者,也就是說很多個人來訂報紙,報社肯定要有相應(yīng)的記錄才行;
出版報紙,這個是報社的主要工作;
發(fā)行報紙,也就是要把出版的報紙發(fā)送到訂閱者手中;
退訂報紙,當(dāng)訂閱者不想要繼續(xù)訂閱了,可以取消訂閱;
上面這些功能是報社最最基本的功能,當(dāng)然,報社還有很多別的功能,為了簡單起見,這里就不再去描述了。因此報社這個目標(biāo)的接口也應(yīng)該實現(xiàn)上述功能,把他們定義在目標(biāo)接口里面,示例代碼如下:
/**
* 目標(biāo)對象,作為被觀察者
*/
public class Subject {
/**
* 用來保存注冊的觀察者對象,也就是報紙的訂閱者
*/
private List<Observer> readers = new ArrayList<Observer>();
/**
* 報紙的讀者需要先向報社訂閱,先要注冊
* @param reader 報紙的讀者
* @return 是否注冊成功
*/
public void attach(Observer reader) {
readers.add(reader);
}
/**
* 報紙的讀者可以取消訂閱
* @param reader 報紙的讀者
* @return 是否取消成功
*/
public void detach(Observer reader) {
readers.remove(reader);
}
/**
* 當(dāng)每期報紙印刷出來后,就要迅速主動的被送到讀者的手中,
* 相當(dāng)于通知讀者,讓他們知道
*/
protected void notifyObservers() {
for(Observer reader : readers){
reader.update(this);
}
}
}
細(xì)心的朋友可能會發(fā)現(xiàn),這個對象并沒有定義出版報紙的功能,這是為了讓這個對象更加通用,這個功能還是有的,放到具體報紙類里面去了
,下面就來具體的看看具體的報紙類的實現(xiàn)。
為了演示簡單,在這個實現(xiàn)類里面增添一個屬性,用它來保存報紙的內(nèi)容,然后增添一個方法來修改這個屬性,修改這個屬性就相當(dāng)于出版了新的報紙,并且同時通知所有的訂閱者。示例代碼如下:
/**
* 報紙對象,具體的目標(biāo)實現(xiàn)
*/
public class NewsPaper extends Subject{
/**
* 報紙的具體內(nèi)容
*/
private String content;
/**
* 獲取報紙的具體內(nèi)容
* @return 報紙的具體內(nèi)容
*/
public String getContent() {
return content;
}
/**
* 示意,設(shè)置報紙的具體內(nèi)容,相當(dāng)于要出版報紙了
* @param content 報紙的具體內(nèi)容
*/
public void setContent(String content) {
this.content = content;
//內(nèi)容有了,說明又出報紙了,那就通知所有的讀者
notifyObservers();
}
}
- 觀察者
目標(biāo)定義好過后,接下來把觀察者抽象出來,看看它應(yīng)該具有什么功能。分析前面的描述,發(fā)現(xiàn)觀察者只要去郵局注冊了過后,就是等著接收報紙就好了,沒有什么其它的功能。那么就把這個接收報紙的功能抽象成為更新的方法,從而定義出觀察者接口來,示例代碼如下:
/**
* 觀察者,比如報紙的讀者
*/
public interface Observer {
/**
* 被通知的方法
* @param subject 具體的目標(biāo)對象,可以獲取報紙的內(nèi)容
*/
public void update(Subject subject);
}
定義好了觀察者的接口過后,該來想想如何實現(xiàn)了。具體的觀察者需要實現(xiàn):在收到被通知的內(nèi)容后,自身如何進(jìn)行相應(yīng)處理的功能。為了演示的簡單,收到報紙內(nèi)容過后,簡單的輸出一下,表示收到了就行了。
定義一個簡單的觀察者實現(xiàn),示例代碼如下:
/**
* 真正的讀者,為了簡單就描述一下姓名
*/
public class Reader implements Observer{
/**
* 讀者的姓名
*/
private String name;
public void update(Subject subject) {
//這是采用拉的方式
System.out.println(name+"收到報紙了,閱讀先。內(nèi)容是==="+((NewsPaper)subject).getContent());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- 使用觀察者模式
前面定義好了觀察者和觀察的目標(biāo),那么如何使用它們呢?
那就寫個客戶端,在客戶端里面,先創(chuàng)建好一個報紙,作為被觀察的目標(biāo),然后多創(chuàng)建幾個讀者作為觀察者,當(dāng)然需要把這些觀察者都注冊到目標(biāo)里面去,接下來就可以出版報紙了,具體的示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建一個報紙,作為被觀察者
NewsPaper subject = new NewsPaper();
//創(chuàng)建閱讀者,也就是觀察者
Reader reader1 = new Reader();
reader1.setName("張三");
Reader reader2 = new Reader();
reader2.setName("李四");
Reader reader3 = new Reader();
reader3.setName("王五");
//注冊閱讀者
subject.attach(reader1);
subject.attach(reader2);
subject.attach(reader3);
//要出報紙啦
subject.setContent("本期內(nèi)容是觀察者模式");
}
}
運行結(jié)果如下:
張三收到報紙了,閱讀先。內(nèi)容是===本期內(nèi)容是觀察者模式
李四收到報紙了,閱讀先。內(nèi)容是===本期內(nèi)容是觀察者模式
王五收到報紙了,閱讀先。內(nèi)容是===本期內(nèi)容是觀察者模式
你還可以通過改變注冊的觀察者,或者是注冊了又退訂,來看看輸出的結(jié)果。會發(fā)現(xiàn)沒有注冊或者退訂的觀察者是收不到報紙的。
如同前面的示例,讀者和報社是一種典型的一對多的關(guān)系,一個報社有多個讀者,當(dāng)報社的狀態(tài)發(fā)生改變,也就是出版新報紙的時候,所有注冊的讀者都會得到通知,然后讀者會拿到報紙,讀者會去閱讀報紙并進(jìn)行后續(xù)的操作。
3 模式講解#
3.1 認(rèn)識觀察者模式##
- 目標(biāo)和觀察者之間的關(guān)系
按照模式的定義,目標(biāo)和觀察者之間是典型的一對多的關(guān)系。
但是要注意,如果觀察者只有一個,也是可以的,這樣就變相實現(xiàn)了目標(biāo)和觀察者之間一對一的關(guān)系,這也使得在處理一個對象的狀態(tài)變化會影響到另一個對象的時候,也可以考慮使用觀察者模式。
同樣的,一個觀察者也可以觀察多個目標(biāo),如果觀察者為多個目標(biāo)定義的通知更新方法都是update方法的話,這會帶來麻煩,因為需要接收多個目標(biāo)的通知,如果是一個update的方法,那就需要在方法內(nèi)部區(qū)分,到底這個更新的通知來自于哪一個目標(biāo),不同的目標(biāo)有不同的后續(xù)操作
。
一般情況下,觀察者應(yīng)該為不同的觀察者目標(biāo),定義不同的回調(diào)方法,這樣實現(xiàn)最簡單,不需要在update方法內(nèi)部進(jìn)行區(qū)分。
- 單向依賴
在觀察者模式中,觀察者和目標(biāo)是單向依賴的,只有觀察者依賴于目標(biāo),而目標(biāo)是不會依賴于觀察者的
。
它們之間聯(lián)系的主動權(quán)掌握在目標(biāo)手中
,只有目標(biāo)知道什么時候需要通知觀察者,在整個過程中,觀察者始終是被動的,被動的等待目標(biāo)的通知,等待目標(biāo)傳值給它。
對目標(biāo)而言,所有的觀察者都是一樣的,目標(biāo)會一視同仁的對待
。當(dāng)然也可以通過在目標(biāo)里面進(jìn)行控制,實現(xiàn)有區(qū)別對待觀察者,比如某些狀態(tài)變化,只需要通知部分觀察者,但那是屬于稍微變形的用法了,不屬于標(biāo)準(zhǔn)的、原始的觀察者模式了。
- 基本的實現(xiàn)說明
具體的目標(biāo)實現(xiàn)對象要能維護(hù)觀察者的注冊信息
,最簡單的實現(xiàn)方案就如同前面的例子那樣,采用一個集合來保存觀察者的注冊信息。
具體的目標(biāo)實現(xiàn)對象需要維護(hù)引起通知的狀態(tài)
,一般情況下是目標(biāo)自身的狀態(tài),變形使用的情況下,也可以是別的對象的狀態(tài)。
具體的觀察者實現(xiàn)對象需要能接收目標(biāo)的通知
,能夠接收目標(biāo)傳遞的數(shù)據(jù),或者是能夠主動去獲取目標(biāo)的數(shù)據(jù),并進(jìn)行后續(xù)處理。
如果是一個觀察者觀察多個目標(biāo),那么在觀察者的更新方法里面,需要去判斷是來自哪一個目標(biāo)的通知
。一種簡單的解決方案就是擴(kuò)展update方法,比如在方法里面多傳遞一個參數(shù)進(jìn)行區(qū)分等;還有一種更簡單的方法,那就是干脆定義不同的回調(diào)方法。
- 命名建議
觀察者模式又被稱為發(fā)布-訂閱模式;
目標(biāo)接口的定義,建議在名稱后面跟Subject;
觀察者接口的定義,建議在名稱后面跟Observer;
觀察者接口的更新方法,建議名稱為update,當(dāng)然方法的參數(shù)可以根據(jù)需要定義,參數(shù)個數(shù)不限、參數(shù)類型不限;
- 觸發(fā)通知的時機(jī)
在實現(xiàn)觀察者模式的時候,一定要注意觸發(fā)通知的時機(jī),一般情況下,是在完成了狀態(tài)維護(hù)后觸發(fā),因為通知會傳遞數(shù)據(jù),不能夠先通知后改數(shù)據(jù)
,這很容易出問題,會導(dǎo)致觀察者和目標(biāo)對象的狀態(tài)不一致。比如:目標(biāo)一發(fā)出通知,就有觀察者來取值,結(jié)果目標(biāo)還沒有更新數(shù)據(jù),這就明顯造成了錯誤。如下示例就是有問題的了,示例代碼如下:
public void setContent(String content) {
//一激動,目標(biāo)先發(fā)出通知了,然后才修改自己的數(shù)據(jù),這會造成問題
notifyAllReader();
this.content = content;
}
- 相互觀察
在某些應(yīng)用里面,可能會出現(xiàn)目標(biāo)和觀察者相互觀察的情況。什么意思呢,比如有兩套觀察者模式的應(yīng)用,其中一套觀察者模式的實現(xiàn)是A對象、B對象觀察C對象;在另一套觀察者模式的實現(xiàn)里面,實現(xiàn)的是B對象、C對象觀察A對象,那么A對象和C對象就是在相互觀察。
換句話說,A對象的狀態(tài)變化會引起C對象的聯(lián)動操作,反過來,C 對象的狀態(tài)變化也會引起A對象的聯(lián)動操作。對于出現(xiàn)這種狀況,要特別小心處理,因為可能會出現(xiàn)死循環(huán)的情況。
- 觀察者模式的調(diào)用順序示意圖
在使用觀察者模式時,會很明顯的分成兩個階段,第一個階段是準(zhǔn)備階段
,也就是維護(hù)目標(biāo)和觀察者關(guān)系的階段,這個階段的調(diào)用順序如圖所示:
接下來就是實際的運行階段了
,這個階段的調(diào)用順序如圖所示:
- 通知的順序
從理論上說,當(dāng)目標(biāo)對象的狀態(tài)變化后通知所有觀察者的時候,順序是不確定的
,因此觀察者實現(xiàn)的功能,絕對不要依賴于通知的順序,也就是說,多個觀察者之間的功能是平行的,相互不應(yīng)該有先后的依賴關(guān)系
。
3.2 推模型和拉模型##
在觀察者模式的實現(xiàn)里面,又分為推模型和拉模型兩種方式,什么意思呢?
- 推模型
目標(biāo)對象主動向觀察者推送目標(biāo)的詳細(xì)信息,不管觀察者是否需要,推送的信息通常是目標(biāo)對象的全部或部分?jǐn)?shù)據(jù),相當(dāng)于是在廣播通信。
- 拉模型
目標(biāo)對象在通知觀察者的時候,只傳遞少量信息,如果觀察者需要更具體的信息,由觀察者主動到目標(biāo)對象中獲取,相當(dāng)于是觀察者從目標(biāo)對象中拉數(shù)據(jù)。
一般這種模型的實現(xiàn)中,會把目標(biāo)對象自身通過update方法傳遞給觀察者,這樣在觀察者需要獲取數(shù)據(jù)的時候,就可以通過這個引用來獲取了。
根據(jù)上面的描述,發(fā)現(xiàn)前面的例子就是典型的拉模型,那么推模型如何實現(xiàn)呢,還是來看個示例吧,這樣會比較清楚。
- 推模型的觀察者接口
根據(jù)前面的講述,推模型通常都是把需要傳遞的數(shù)據(jù)直接推送給觀察者對象
,所以觀察者接口中的update方法的參數(shù)需要發(fā)生變化,示例代碼如下:
/**
* 觀察者,比如報紙的讀者
*/
public interface Observer {
/**
* 被通知的方法,直接把報紙的內(nèi)容推送過來
* @param content 報紙的內(nèi)容
*/
public void update(String content);
}
- 推模型的觀察者的具體實現(xiàn)
以前需要到目標(biāo)對象里面獲取自己需要的數(shù)據(jù),現(xiàn)在是直接接收傳入的數(shù)據(jù),這就是改變的地方,示例代碼如下:
public class Reader implements Observer{
/**
* 讀者的姓名
*/
private String name;
public void update(String content) {
//這是采用推的方式
System.out.println(name+"收到報紙了,閱讀先。內(nèi)容是==="+content);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- 推模型的目標(biāo)對象
跟拉模型的目標(biāo)實現(xiàn)相比,有一些變化:
一個就是通知所有觀察者的方法,以前是沒有參數(shù)的,現(xiàn)在需要傳入需要主動推送的數(shù)據(jù);
另外一個就是在循環(huán)通知觀察者的時候,也就是循環(huán)調(diào)用觀察者的update方法的時候,傳入的參數(shù)不同了;
示例代碼如下:
/**
* 目標(biāo)對象,作為被觀察者,使用推模型
*/
public class Subject {
/**
* 用來保存注冊的觀察者對象,也就是報紙的訂閱者
*/
private List<Observer> readers = new ArrayList<Observer>();
/**
* 報紙的讀者需要先向報社訂閱,先要注冊
* @param reader 報紙的讀者
* @return 是否注冊成功
*/
public void attach(Observer reader) {
readers.add(reader);
}
/**
* 報紙的讀者可以取消訂閱
* @param reader 報紙的讀者
* @return 是否取消成功
*/
public void detach(Observer reader) {
readers.remove(reader);
}
/**
* 當(dāng)每期報紙印刷出來后,就要迅速的主動的被送到讀者的手中,
* 相當(dāng)于通知讀者,讓他們知道
* @param content 要主動推送的內(nèi)容
*/
protected void notifyObservers(String content) {
for(Observer reader : readers){
reader.update(content);
}
}
}
- 推模型的目標(biāo)具體實現(xiàn)
跟拉模型相比,有一點變化,就是在調(diào)用通知觀察者的方法的時候,需要傳入?yún)?shù)了,拉模型的實現(xiàn)中是不需要的,示例代碼如下:
public class NewsPaper extends Subject{
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
//內(nèi)容有了,說明又出報紙了,那就通知所有的讀者
notifyObservers(content);
}
}
- 推模型的客戶端使用
跟拉模型一樣,沒有變化。
- 關(guān)于兩種模型的比較
兩種實現(xiàn)模型,在開發(fā)的時候,究竟應(yīng)該使用哪一種,還是應(yīng)該具體問題具體分析。這里,只是把兩種模型進(jìn)行一個簡單的比較。
推模型是假定目標(biāo)對象知道觀察者需要的數(shù)據(jù);而拉模型是目標(biāo)對象不知道觀察者具體需要什么數(shù)據(jù),沒有辦法的情況下,干脆把自身傳給觀察者,讓觀察者自己去按需取值。
推模型可能會使得觀察者對象難以復(fù)用,因為觀察者定義的update方法是按需而定義的,可能無法兼顧沒有考慮到的使用情況。這就意味著出現(xiàn)新情況的時候,就可能需要提供新的update方法,或者是干脆重新實現(xiàn)觀察者。
而拉模型就不會造成這樣的情況,因為拉模型下,update方法的參數(shù)是目標(biāo)對象本身,這基本上是目標(biāo)對象能傳遞的最大數(shù)據(jù)集合了,基本上可以適應(yīng)各種情況的需要。
3.3 Java中的觀察者模式##
估計有些朋友在看前面的內(nèi)容的時候,心里就嘀咕上了,Java里面不是已經(jīng)有了觀察者模式的部分實現(xiàn)嗎,為何還要全部自己從頭做呢?
主要是為了讓大家更好的理解觀察者模式本身,而不用受Java語言實現(xiàn)的限制。
好了,下面就來看看如何利用Java中已有的功能來實現(xiàn)觀察者模式。在java.util包里面有一個類Observable,它實現(xiàn)了大部分我們需要的目標(biāo)的功能
;還有一個接口Observer,它里面定義了update的方法,就是觀察者的接口
。
因此,利用Java中已有的功能來實現(xiàn)觀察者模式非常簡單,跟前面完全由自己來實現(xiàn)觀察者模式相比有如下改變:
不需要再定義觀察者和目標(biāo)的接口了,JDK幫忙定義了;
具體的目標(biāo)實現(xiàn)里面不需要再維護(hù)觀察者的注冊信息了,這個在Java中的Observable類里面,已經(jīng)幫忙實現(xiàn)好了;
觸發(fā)通知的方式有一點變化,要先調(diào)用setChanged方法,這個是Java為了幫助實現(xiàn)更精確的觸發(fā)控制而提供的功能;
具體觀察者的實現(xiàn)里面,update方法其實能同時支持推模型和拉模型,這個是Java在定義的時候,就已經(jīng)考慮進(jìn)去了;
好了,說了這么多,還是看看例子會比較直觀。
- 新的目標(biāo)的實現(xiàn),不再需要自己來實現(xiàn)Subject定義,在具體實現(xiàn)的時候,也不是繼承Subject了,而是改成繼承Java中定義的Observable,示例代碼如下:
/**
* 報紙對象,具體的目標(biāo)實現(xiàn)
*/
public class NewsPaper extends java.util.Observable {
/**
* 報紙的具體內(nèi)容
*/
private String content;
/**
* 獲取報紙的具體內(nèi)容
* @return 報紙的具體內(nèi)容
*/
public String getContent() {
return content;
}
/**
* 示意,設(shè)置報紙的具體內(nèi)容,相當(dāng)于要出版報紙了
* @param content 報紙的具體內(nèi)容
*/
public void setContent(String content) {
this.content = content;
//內(nèi)容有了,說明又出新報紙了,那就通知所有的讀者
//注意在用Java中的Observer模式的時候,下面這句話不可少
this.setChanged();
//然后主動通知,這里用的是推的方式
this.notifyObservers(this.content);
//如果用拉的方式,這么調(diào)用
//this.notifyObservers();
}
}
- 再看看新的觀察者的實現(xiàn),不是實現(xiàn)自己定義的觀察者接口,而是實現(xiàn)由Java提供的Observer接口,示例代碼如下:
/**
* 真正的讀者,為了簡單就描述一下姓名
*/
public class Reader implements java.util.Observer {
/**
* 讀者的姓名
*/
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void update(Observable o, Object obj) {
//這是采用推的方式
System.out.println(name+"收到報紙了,閱讀先。目標(biāo)推過來的內(nèi)容是==="+obj);
//這是獲取拉的數(shù)據(jù)
System.out.println(name+"收到報紙了,閱讀先。主動到目標(biāo)對象去拉的內(nèi)容是==="+((NewsPaper)o).getContent());
}
}
- 客戶端使用
客戶端跟前面的寫法沒有太大改變,主要在注冊閱讀者的時候,調(diào)用的方法跟以前不一樣了,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建一個報紙,作為被觀察者
NewsPaper subject = new NewsPaper();
//創(chuàng)建閱讀者,也就是觀察者
Reader reader1 = new Reader();
reader1.setName("張三");
Reader reader2 = new Reader();
reader2.setName("李四");
Reader reader3 = new Reader();
reader3.setName("王五");
//注冊閱讀者
subject.addObserver(reader1);
subject.addObserver(reader2);
subject.addObserver(reader3);
//要出報紙啦
subject.setContent("本期內(nèi)容是觀察者模式");
}
}
趕緊測試一下,運行運行,看看結(jié)果,運行結(jié)果如下所示:
王五收到報紙了,閱讀先。目標(biāo)推過來的內(nèi)容是===本期內(nèi)容是觀察者模式
王五收到報紙了,閱讀先。主動到目標(biāo)對象去拉的內(nèi)容是===本期內(nèi)容是觀察者模式
李四收到報紙了,閱讀先。目標(biāo)推過來的內(nèi)容是===本期內(nèi)容是觀察者模式
李四收到報紙了,閱讀先。主動到目標(biāo)對象去拉的內(nèi)容是===本期內(nèi)容是觀察者模式
張三收到報紙了,閱讀先。目標(biāo)推過來的內(nèi)容是===本期內(nèi)容是觀察者模式
張三收到報紙了,閱讀先。主動到目標(biāo)對象去拉的內(nèi)容是===本期內(nèi)容是觀察者模式
然后好好對比自己實現(xiàn)觀察者模式和使用Java已有的功能來實現(xiàn)觀察者模式,看看有什么不同,有什么相同,好好體會一下。
3.4 觀察者模式的優(yōu)缺點##
- 觀察者模式實現(xiàn)了觀察者和目標(biāo)之間的抽象耦合
原本目標(biāo)對象在狀態(tài)發(fā)生改變的時候,需要直接調(diào)用所有的觀察者對象,但是抽象出觀察者接口過后,目標(biāo)和觀察者就只是在抽象層面上耦合了,也就是說目標(biāo)只是知道觀察者接口,并不知道具體的觀察者的類,從而實現(xiàn)目標(biāo)類和具體的觀察者類之間解耦。
- 觀察者模式實現(xiàn)了動態(tài)聯(lián)動
所謂聯(lián)動,就是做一個操作會引起其它相關(guān)的操作。由于觀察者模式對觀察者注冊實行管理,那就可以在運行期間,通過動態(tài)的控制注冊的觀察者,來控制某個動作的聯(lián)動范圍,從而實現(xiàn)動態(tài)聯(lián)動。
- 觀察者模式支持廣播通信
由于目標(biāo)發(fā)送通知給觀察者是面向所有注冊的觀察者,所以每次目標(biāo)通知的信息就要對所有注冊的觀察者進(jìn)行廣播。當(dāng)然,也可以通過在目標(biāo)上添加新的功能來限制廣播的范圍。
在廣播通信的時候要注意一個問題,就是相互廣播造成死循環(huán)的問題。比如A和B兩個對象互為觀察者和目標(biāo)對象,A對象發(fā)生狀態(tài)變化,然后A來廣播信息,B對象接收到通知后,在處理過程中,使得B對象的狀態(tài)也發(fā)生了改變,然后B來廣播信息,然后A對象接到通知后,又觸發(fā)廣播信息……,如此A引起B(yǎng)變化,B又引起A變化,從而一直相互廣播信息,就造成死循環(huán)了。
- 觀察者模式可能會引起無謂的操作
由于觀察者模式每次都是廣播通信,不管觀察者需不需要,每個觀察者都會被調(diào)用update方法,如果觀察者不需要執(zhí)行相應(yīng)處理,那么這次操作就浪費了。
其實浪費了還好,怕就怕引起了誤更新,那就麻煩了,比如:本應(yīng)該在執(zhí)行這次狀態(tài)更新前把某個觀察者刪除掉,這樣通知的時候就沒有這個觀察者了,但是現(xiàn)在忘掉了,那么就會引起誤操作。
3.5 思考觀察者模式##
- 觀察者模式的本質(zhì)
觀察者模式的本質(zhì):觸發(fā)聯(lián)動。
當(dāng)修改目標(biāo)對象的狀態(tài)的時候,就會觸發(fā)相應(yīng)的通知,然后會循環(huán)調(diào)用所有注冊的觀察者對象的相應(yīng)方法,其實就相當(dāng)于聯(lián)動調(diào)用這些觀察者的方法。
而且這個聯(lián)動還是動態(tài)的,可以通過注冊和取消注冊來控制觀察者,因而可以在程序運行期間,通過動態(tài)的控制觀察者,來變相的實現(xiàn)添加和刪除某些功能處理,這些功能就是觀察者在update的時候執(zhí)行的功能。
同時目標(biāo)對象和觀察者對象的解耦,又保證了無論觀察者發(fā)生怎樣的變化,目標(biāo)對象總是能夠正確地聯(lián)動過來。
理解這個本質(zhì)對我們非常有用,對于我們識別和使用觀察者模式有非常重要的意義,尤其是在變形使用的時候,萬變不離其宗。
- 何時選用觀察者模式
建議在如下情況中,選用觀察者模式:
當(dāng)一個抽象模型有兩個方面,其中一個方面的操作依賴于另一個方面的狀態(tài)變化,那么就可以選用觀察者模式,將這兩者封裝成觀察者和目標(biāo)對象,當(dāng)目標(biāo)對象變化的時候,依賴于它的觀察者對象也會發(fā)生相應(yīng)的變化。這樣就把抽象模型的這兩個方面分離開了,使得它們可以獨立的改變和復(fù)用。
如果在更改一個對象的時候,需要同時連帶改變其它的對象,而且不知道究竟應(yīng)該有多少對象需要被連帶改變,這種情況可以選用觀察者模式,被更改的那一個對象很明顯就相當(dāng)于是目標(biāo)對象,而需要連帶修改的多個其它對象,就作為多個觀察者對象了。
當(dāng)一個對象必須通知其它的對象,但是你又希望這個對象和其它被它通知的對象是松散耦合的,也就是說這個對象其實不想知道具體被通知的對象,這種情況可以選用觀察者模式,這個對象就相當(dāng)于是目標(biāo)對象,而被它通知的對象就是觀察者對象了。
3.6 Swing中的觀察者模式##
Java的Swing中到處都是觀察者模式的身影,比如大家熟悉的事件處理,就是典型的觀察者模式的應(yīng)用。(說明一下:早期的Swing事件處理用的是職責(zé)鏈)
Swing組件是被觀察的目標(biāo),而每個實現(xiàn)監(jiān)聽器的類就是觀察者,監(jiān)聽器的接口就是觀察者的接口,在調(diào)用addXXXListener方法的時候就相當(dāng)于注冊觀察者。
當(dāng)組件被點擊,狀態(tài)發(fā)生改變的時候,就會產(chǎn)生相應(yīng)的通知,會調(diào)用注冊的觀察者的方法,就是我們所實現(xiàn)的監(jiān)聽器的方法。
從這里還可以學(xué)一招:如何處理一個觀察者觀察多個目標(biāo)對象?
你看一個Swing的應(yīng)用程序,作為一個觀察者,經(jīng)常會注冊觀察多個不同的目標(biāo)對象
,也就是同一類,既實現(xiàn)了按鈕組件的事件處理,又實現(xiàn)了文本框組件的事件處理,是怎么做到的呢?
答案就在監(jiān)聽器接口上,這些監(jiān)聽器接口就相當(dāng)于觀察者接口,也就是說一個觀察者要觀察多個目標(biāo)對象,只要不同的目標(biāo)對象使用不同的觀察者接口就好了
,當(dāng)然,這些接口里面的方法也不相同,不再都是update方法了。這樣一來,不同的目標(biāo)對象通知觀察者所調(diào)用的方法也就不同了,這樣在具體實現(xiàn)觀察者的時候,也就實現(xiàn)成不同的方法,自然就區(qū)分開了。
3.7 簡單變形示例——區(qū)別對待觀察者##
首先聲明,這里只是舉一個非常簡單的變形使用的例子,也可算是基本的觀察者模式的功能加強(qiáng),事實上可以有很多很多的變形應(yīng)用,這也是為什么我們特別強(qiáng)調(diào)大家要深入理解每個設(shè)計模式,要把握每個模式的本質(zhì)的原因了。
- 范例需求
這是一個實際系統(tǒng)的簡化需求:在一個水質(zhì)監(jiān)測系統(tǒng)中有這樣一個功能,當(dāng)水中的雜質(zhì)為正常的時候,只是通知監(jiān)測人員做記錄;當(dāng)為輕度污染的時候,除了通知監(jiān)測人員做記錄外,還要通知預(yù)警人員,判斷是否需要預(yù)警;當(dāng)為中度或者高度污染的時候,除了通知監(jiān)測人員做記錄外,還要通知預(yù)警人員,判斷是否需要預(yù)警,同時還要通知監(jiān)測部門領(lǐng)導(dǎo)做相應(yīng)的處理。
- 解決思路和范例代碼
分析上述需求就會發(fā)現(xiàn),對于水質(zhì)污染這件事情,有可能會涉及到監(jiān)測員、預(yù)警人員、監(jiān)測部門領(lǐng)導(dǎo),根據(jù)不同的水質(zhì)污染情況涉及到不同的人員,也就是說,監(jiān)測員、預(yù)警人員、監(jiān)測部門領(lǐng)導(dǎo)他們?nèi)呤瞧叫械模氊?zé)都是處理水質(zhì)污染,但是處理的范圍不一樣。
因此很容易套用上觀察者模式,如果把水質(zhì)污染的記錄當(dāng)作被觀察的目標(biāo)的話,那么監(jiān)測員、預(yù)警人員和監(jiān)測部門領(lǐng)導(dǎo)就都是觀察者了。
前面學(xué)過的觀察者模式,當(dāng)目標(biāo)通知觀察者的時候是全部都通知,但是現(xiàn)在這個需求是不同的情況來讓不同的人處理,怎么辦呢?
解決的方式通常有兩種,一種是目標(biāo)可以通知,但是觀察者不做任何操作;另外一種是在目標(biāo)里面進(jìn)行判斷,干脆就不通知了。兩種實現(xiàn)方式各有千秋,這里選擇后面一種方式來示例,這種方式能夠統(tǒng)一邏輯控制,并進(jìn)行觀察者的統(tǒng)一分派,有利于業(yè)務(wù)控制和今后的擴(kuò)展。
(1)先來定義觀察者的接口,這個接口跟前面的示例差別也不大,只是新加了訪問觀察人員職務(wù)的方法,示例代碼如下:
/**
* 水質(zhì)觀察者接口定義
*/
public interface WatcherObserver {
/**
* 被通知的方法
* @param subject 傳入被觀察的目標(biāo)對象
*/
public void update(WaterQualitySubject subject);
/**
* 設(shè)置觀察人員的職務(wù)
* @param job 觀察人員的職務(wù)
*/
public void setJob(String job);
/**
* 獲取觀察人員的職務(wù)
* @return 觀察人員的職務(wù)
*/
public String getJob();
}
(2)定義完接口后,來看看觀察者的具體實現(xiàn),示例代碼如下:
/**
* 具體的觀察者實現(xiàn)
*/
public class Watcher implements WatcherObserver{
/**
* 職務(wù)
*/
private String job;
public String getJob() {
return this.job;
}
public void setJob(String job) {
this.job = job;
}
public void update(WaterQualitySubject subject) {
//這里采用的是拉的方式
System.out.println(job+"獲取到通知,當(dāng)前污染級別為:"+subject.getPolluteLevel());
}
}
(3)接下來定義目標(biāo)的父對象,跟以前相比有些改變:
把父類實現(xiàn)成抽象的,因為在里面要定義抽象的方法;
原來通知所有的觀察者的方法被去掉了,這個方法現(xiàn)在需要由子類去實現(xiàn),要按照業(yè)務(wù)來有區(qū)別的來對待觀察者,得看看是否需要通知觀察者;
新添加一個水質(zhì)污染級別的業(yè)務(wù)方法,這樣在觀察者獲取目標(biāo)對象的數(shù)據(jù)的時候,就不需要再知道具體的目標(biāo)對象,也不需要強(qiáng)制造型了;
/**
* 定義水質(zhì)監(jiān)測的目標(biāo)對象
*/
public abstract class WaterQualitySubject {
/**
* 用來保存注冊的觀察者對象
*/
protected List<WatcherObserver> observers = new ArrayList<WatcherObserver>();
/**
* 注冊觀察者對象
* @param observer 觀察者對象
*/
public void attach(WatcherObserver observer) {
observers.add(observer);
}
/**
* 刪除觀察者對象
* @param observer 觀察者對象
*/
public void detach(WatcherObserver observer) {
observers.remove(observer);
}
/**
* 通知相應(yīng)的觀察者對象
*/
public abstract void notifyWatchers();
/**
* 獲取水質(zhì)污染的級別
* @return 水質(zhì)污染的級別
*/
public abstract int getPolluteLevel();
}
(4)接下來重點看看目標(biāo)的實現(xiàn),在目標(biāo)對象里面,添加一個描述污染級別的屬性,在判斷是否需要通知觀察者的時候,不同的污染程度對應(yīng)會通知不同的觀察者,示例代碼如下:
/**
* 具體的水質(zhì)監(jiān)測對象
*/
public class WaterQuality extends WaterQualitySubject{
/**
* 污染的級別,0表示正常,1表示輕度污染,2表示中度污染,3表示高度污染
*/
private int polluteLevel = 0;
/**
* 獲取水質(zhì)污染的級別
* @return 水質(zhì)污染的級別
*/
public int getPolluteLevel() {
return polluteLevel;
}
/**
* 當(dāng)監(jiān)測水質(zhì)情況后,設(shè)置水質(zhì)污染的級別
* @param polluteLevel 水質(zhì)污染的級別
*/
public void setPolluteLevel(int polluteLevel) {
this.polluteLevel = polluteLevel;
//通知相應(yīng)的觀察者
this.notifyWatchers();
}
/**
* 通知相應(yīng)的觀察者對象
*/
public void notifyWatchers() {
//循環(huán)所有注冊的觀察者
for(WatcherObserver watcher : observers){
//開始根據(jù)污染級別判斷是否需要通知,由這里總控
if(this.polluteLevel >= 0){
//通知監(jiān)測員做記錄
if("監(jiān)測人員".equals(watcher.getJob())){
watcher.update(this);
}
}
if(this.polluteLevel >= 1){
//通知預(yù)警人員
if("預(yù)警人員".equals(watcher.getJob())){
watcher.update(this);
}
}
if(this.polluteLevel >= 2){
//通知監(jiān)測部門領(lǐng)導(dǎo)
if("監(jiān)測部門領(lǐng)導(dǎo)".equals(watcher.getJob())){
watcher.update(this);
}
}
}
}
}
(5)大功告成,來寫個客戶端,測試一下,示例代碼如下:
public class Client {
public static void main(String[] args) {
//創(chuàng)建水質(zhì)主題對象
WaterQuality subject = new WaterQuality();
//創(chuàng)建幾個觀察者
WatcherObserver watcher1 = new Watcher();
watcher1.setJob("監(jiān)測人員");
WatcherObserver watcher2 = new Watcher();
watcher2.setJob("預(yù)警人員");
WatcherObserver watcher3 = new Watcher();
watcher3.setJob("監(jiān)測部門領(lǐng)導(dǎo)");
//注冊觀察者
subject.attach(watcher1);
subject.attach(watcher2);
subject.attach(watcher3);
//填寫水質(zhì)報告
System.out.println("當(dāng)水質(zhì)為正常的時候------------------〉");
subject.setPolluteLevel(0);
System.out.println("當(dāng)水質(zhì)為輕度污染的時候---------------〉");
subject.setPolluteLevel(1);
System.out.println("當(dāng)水質(zhì)為中度污染的時候---------------〉");
subject.setPolluteLevel(2);
}
}
(6)運行一下,看看結(jié)果,如下:
當(dāng)水質(zhì)為正常的時候------------------〉
監(jiān)測人員獲取到通知,當(dāng)前污染級別為:0
當(dāng)水質(zhì)為輕度污染的時候---------------〉
監(jiān)測人員獲取到通知,當(dāng)前污染級別為:1
預(yù)警人員獲取到通知,當(dāng)前污染級別為:1
當(dāng)水質(zhì)為中度污染的時候---------------〉
監(jiān)測人員獲取到通知,當(dāng)前污染級別為:2
預(yù)警人員獲取到通知,當(dāng)前污染級別為:2
監(jiān)測部門領(lǐng)導(dǎo)獲取到通知,當(dāng)前污染級別為:2
仔細(xì)觀察上面輸出的結(jié)果,你會發(fā)現(xiàn),當(dāng)填寫不同的污染級別時,被通知的人員是不同的。但是這些觀察者是不知道這些不同的,觀察者只是在自己獲得通知的時候去執(zhí)行自己的工作。具體要不要通知,什么時候通知都是目標(biāo)對象的工作
。
3.8 相關(guān)模式##
- 觀察者模式和狀態(tài)模式
觀察者模式和狀態(tài)模式是有相似之處的。
觀察者模式是當(dāng)目標(biāo)狀態(tài)發(fā)生改變時,觸發(fā)并通知觀察者,讓觀察者去執(zhí)行相應(yīng)的操作。而狀態(tài)模式是根據(jù)不同的狀態(tài),選擇不同的實現(xiàn),這個實現(xiàn)類的主要功能就是針對狀態(tài)的相應(yīng)的操作,它不像觀察者,觀察者本身還有很多其它的功能,接收通知并執(zhí)行相應(yīng)處理只是觀察者的部分功能。
當(dāng)然觀察者模式和狀態(tài)模式是可以結(jié)合使用的。觀察者模式的重心在觸發(fā)聯(lián)動,但是到底決定哪些觀察者會被聯(lián)動,這時就可以采用狀態(tài)模式來實現(xiàn)了,也可以采用策略模式來進(jìn)行選擇需要聯(lián)動的觀察者
。
- 觀察者模式和中介者模式
觀察者模式和中介者模式是可以結(jié)合使用的。
前面的例子中目標(biāo)都只是簡單的通知一下,然后讓各個觀察者自己去完成更新就結(jié)束了。如果觀察者和被觀察的目標(biāo)之間的交互關(guān)系很復(fù)雜,比如:有一個界面,里面有三個下拉列表組件,分別是選擇國家、省份/州、具體的城市,很明顯這是一個三級聯(lián)動,當(dāng)你選擇一個國家的時候,省份/州應(yīng)該相應(yīng)改變數(shù)據(jù),省份/州一改變,具體的城市也需要改變。
這種情況下,很明顯需要相關(guān)的狀態(tài)都聯(lián)動準(zhǔn)備好了,然后再一次性的通知觀察者,就是界面做更新處理,不會國家改變一下,省份和城市還沒有改,就通知界面更新。這種情況就可以使用中介者模式來封裝觀察者和目標(biāo)的關(guān)系。
在使用Swing的小型應(yīng)用里面,也可以使用中介者模式。比如:把一個界面所有的事件用一個對象來處理,把一個組件觸發(fā)事件過后,需要操作其它組件的動作都封裝到一起,這個對象就是典型的中介者。