設計模式私人筆記-觀察者模式

前言:設計模式,一個老掉牙的話題,一個永恆的話題。所有的Java開發,都必須經過的話題,都必須學習的話題。但是其實很多時候自己只是看了,瞭解了,然後忘了。所以還是想從頭梳理一遍重要的設計模式,給自己看。不求邏輯多清晰,內容多完善,UML圖畫的多嚴謹,理解、會意就好。

遇到什麼問題

提出一個經典的需求:假設現在有一個報價系統,當某個產品的價格發生變化時,就會通過短信發送給相關客戶,也會通過郵件給該客戶發送價格變動情況。

用最經典的方法來實現該需求。這裡只是做了一個非常簡化的實現。

創建兩個服務,一個用於發送短信,一個用於發送郵件:


設計模式私人筆記-觀察者模式

設計模式私人筆記-觀察者模式

創建一個服務,用於處理價格變動的邏輯:


設計模式私人筆記-觀察者模式

基於該設計的使用代碼:


設計模式私人筆記-觀察者模式

該設計方式非常簡單,也是非常非常常見的實現方案。但是該方案很明顯存在一些問題:

· 兩個感知價格變動的服務SmsSender和MailSender,沒有抽象出接口,雖然感知價格的方法一致;

· 如果要新添加感知價格變動的服務,比如我現在要求把每次變動的價格計入日誌系統,又需要增加一個實體類 PriceLogger;並且,每次增加新的價格變動感知服務,都要添加為PublicPriceService的成員,然後由PublicPriceService手動去依次調用;

簡單來說,就是,這種實現方案,擴展性太差!

應用觀察者模式

先簡單說明一下什麼是觀察者模式。舉個經典的案例:公眾號的關注。當一個用戶關注了某個公眾號,就相當於該公眾號把用戶添加到了用戶列表,這樣一來,只要該公眾號有消息更新,即可按照用戶列表中的用戶依次推送消息。如果用戶不願意收到該公眾號的信息,就取消關注即可。

在Head First Java Pattern一書中,把觀察者模式定義為:

觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,他所有依賴者都會收到通知並執行更新。

設計模式私人筆記-觀察者模式


· 有狀態對象的狀態更新;

· 通知所有註冊的觀察者進行更新; 其實就這兩步。下面先用觀察者模式重寫上面的案例。

首先為主題對象(有狀態對象)定義一個接口:


設計模式私人筆記-觀察者模式

· 注意,第一個方法用於註冊價格感知對象,即之前的SmsSender,MailSender,或者其他的需要監控價格變化的對象;

第二步,為所有需要感知價格變化的業務統一抽取一個接口:


設計模式私人筆記-觀察者模式

該接口中就一個方法,執行價格的改變感應;那麼方法傳入的price就是變化之後的price;

然後重新實現發送郵件和短信的服務:


設計模式私人筆記-觀察者模式


設計模式私人筆記-觀察者模式

在之前的版本之上,只是實現了PriceChangeNotifier接口;

最後,重新修改我們的價格改變對象:


設計模式私人筆記-觀察者模式

在之前的版本基礎上,實現了PriceChanger接口,因為一對多,先簡單的使用List來存放。在notifyChangers方法中,分別調用註冊的每一個價格感知器的業務方法。

這個版本的測試代碼:


設計模式私人筆記-觀察者模式

好處

· 完全面向接口編程;

· 擴展方便,如果我現在要增加記錄日誌的功能,只需要:


設計模式私人筆記-觀察者模式

不需要修改PublicPriceService本身的代碼,只需要在執行之前,把PriceLogger註冊到PublicPriceService中:


設計模式私人筆記-觀察者模式

即可。

標準的觀察者模式

上面的例子,僅僅只是應用觀察者模式的案例,現在來抽象出標準的觀察者模式,看類圖:

設計模式私人筆記-觀察者模式


有幾個點注意:

· Subject:抽象主題(接口),就是包含狀態發生改變的那個類的抽象;在該接口中,定義了添加觀察者,刪除觀察者和通知觀察者三個方法;

· ConcreateSubject:主題接口的實現;

· Observer:抽象的觀察者(接口),就是上面說到的感知改變的對象的抽象,定義了一個感知改變之後的業務邏輯方法。這個方法中定義了兩個參數,Subject(本次產生改變的主題對象),Object(本次變化的數據),關於這兩個參數,涉及到改變數據的推送和拉取兩種模式,後面介紹,這裡就簡單理解為用這兩個參數來獲取改變的數據就可以了(第二個Object看成上面的BigDecimal即可)。

下面寫一段標準觀察者模式示例代碼。

定義Subject接口:


設計模式私人筆記-觀察者模式


定義觀察者接口:


設計模式私人筆記-觀察者模式

定義具體的主題類實現,仍然先使用簡單的List來存儲註冊的觀察者;


設計模式私人筆記-觀察者模式

最後做一個觀察者的實現:


設計模式私人筆記-觀察者模式


使用起來也很簡單,只需要和上面示例中一樣,創建好具體的主題對象和具體的觀察者,將觀察者註冊到主題對象中即可。

Java中的觀察者模式

因為觀察者模式使用非常頻繁,甚至被譽為設計模式的皇后(不做評價),所以Java其實已經提供了觀察者模式的實現。

在java.util包中,有兩個類型:

· java.util.Observable:可被觀察對象,即我們的Subject;

· java.util.Observer:觀察對象,就是我們上面的Observer;

我們來看看這兩個類的結構,有一個直觀瞭解:


設計模式私人筆記-觀察者模式


首先,Observer接口的實現和我們上面標準的觀察者接口定義相同,仍然支持推送和拉取兩種獲取變化數據的方式,不過多說明;

下面重點看看Observable定義:

設計模式私人筆記-觀察者模式


注意到幾個點: 1,首先,Observable是一個抽象類。我們知道,不建議將這種抽象定義為抽象類,會讓我們的類組織關係變得非常死板,這是設計上的一個問題,我們先跳過(這個類是JDK1.0就存在了,兼容問題沒有更新,後面會介紹更好的方法)。 2,我們看到了addObserver和deleteObserver方法,很明顯,這兩個方法用於維護觀察者; 3,deleteObservers方法,顧名思義,用於移除掉所有該主題上的觀察者; 4,notifyObservers方法和notifyObservers(Object)兩個方法,很明顯都是告知觀察者的方法,只是一個可以主動傳遞改變的值(推送),一個不需要傳遞改變的值(這個值可以主題對象通過getter方法提供,即拉取)。 4,setChanged,clearChanged和hasChanged,這三個方法比較奇怪,肯定是屬於一組內容的,具體是什麼,我們後面介紹。這裡只需要注意一個重點,就是在我們調用notifyObservers方法之前,我們必須調用setChanged方法,告知我們的改變已經成立(為什麼這麼設計,後面討論)

如果還有點模糊,我們直接使用java提供的觀察者模式支持類,完成我們的價格監控的實例:

從最簡單的發送郵件服務開始:


設計模式私人筆記-觀察者模式


我們只需要實現Observer接口,然後在update方法中完成邏輯即可。這裡需要注意,我們採用的是推送的方式,即我們直接從第二個參數中獲取了price的數值,那麼意味著主題對象中,需要調用notifyObservers(Object)方法來推送數據(注意觀察後面的代碼);

記錄日誌服務:


設計模式私人筆記-觀察者模式


接下來,主題對象的實現:


設計模式私人筆記-觀察者模式


注意代碼中的註釋,我們在notifyObservers方法調用之前,一定要調用setChanged方法。

基於Java的觀察者模式的測試代碼:


設計模式私人筆記-觀察者模式


測試代碼很簡單。

小結,那麼Java提供的觀察者模式到底為我們做了些什麼?很明顯,主題對觀察者的維護工作,主題對觀察者的通知動作,都放到了Observable類中。那我們來簡單分析一下Observable類的源碼,主要解決兩個問題:

· 怎麼維護觀察者;

· setChange方法到底用來幹嘛;

Observable類源碼分析


設計模式私人筆記-觀察者模式


· Observable維護兩個成員變量,changed和obs,其中changed代表數據是否確定改變,obs是一個Observer的Vector,Vector雖然性能差,但確是線程安全的,這點是選用Vector的原因。

· 構造方法中初始化Vector;


設計模式私人筆記-觀察者模式

· 這裡列出了和觀察者相關的所有方法,首先注意,方法都是synchronized的,保證了線程安全;


設計模式私人筆記-觀察者模式


· 這是關於通知觀察者,調用觀察者更新方法的代碼。在代碼註釋中已經說明兩個方法的區別;

· 在notifyObservers方法中,我們注意到聲明瞭一個局部變量Object[] arrLocal;並使用arrLocal = obs.toArray();將當前Vector中的觀察者拷貝到了arrLocal中,最後在執行通知的時候,遍歷的是arrLocal中的觀察者引用;這樣做的目的在於,使用arrLocal作為一個當前執行通知時的觀察者快照,那麼,在真正只是update方法的時候,不會造成Vector正常的添加和刪除;對併發下的性能有一定幫助;但是可能會存在兩個問題需要注意:

· 可能會有剛加入的觀察者沒法及時獲取正在變更的數據;

· 可能會有剛刪除的觀察者仍然受到了最近正在變更的數據;

· 代碼中,使用if(!changed)return 來判定數據是否真正改變。接下來看看changed相關的方法:


設計模式私人筆記-觀察者模式

使用了三個方法來操作changed變量,代碼很簡單。但是我們通過這兩段代碼,也能清楚的看出,為什麼我們在調用notifyObservers方法之前,一定要調用setChanged方法了。

可能有童鞋會覺得這樣的設計有點多此一舉,但是考慮到,如果一個數據變化過於頻繁,那麼我們在某些情況下,也可以通過changed變量來控制真正的發送數據變更消息的時機,這也是一個設計初衷吧。

Java的Observer體系有什麼問題?

上面已經看了Java本身提供的觀察者模式的具體使用和相關代碼實現,那麼,這樣設計有問題麼?

回答是肯定的。有問題。所以其實我們發現,真正使用Java的Observer體系的代碼也不多。主要原因我認為有兩個: 1,Observer是一個類,這個本身就是很煩的事情,我的主題類需要繼承它,代碼結構會有問題; 2,Observer中重要的方法,比如setChanged方法,導致除了繼承,沒法在外部通過第三方代碼來促使主題確定改變,在一些場景中,有較大侷限性。

一些相關話題

上面已經對觀察者模式做了較完整的梳理,完了麼?還沒有。從觀察者,我們至少能夠延伸出下面兩個問題:

· 觀察者模式和事件監聽模式有什麼關係?

· 觀察者模式和發佈訂閱模式有什麼關係?

這裡又引出了兩個模式:事件監聽模式和發佈訂閱模式,這兩個模式又可以引申出一大堆問題。我們在分文章來獨立闡述。


分享到:


相關文章: