天天看點

設計模式之觀察者模式

觀察者模式又稱為釋出-訂閱(Publish/Subscribe)模式,是23種設計模式之一。DP中是這麼定義觀察者模式的:

觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀态發生變化時,會通知所有的觀察者對象,使它們能夠自動更新自己。

舉個生活中的例子,例如在某班級裡,幾個同學都在某個網站上訂閱了一本連載的漫畫。當漫畫更新時,就會通知這幾位同學,同學收到通知後就可以去下載下傳漫畫的最新篇章。這就是一個典型的觀察者模式,在這裡同學都是觀察者,而漫畫則是他們共同監聽的一個主題,而漫畫更新時也就是主題對象發生變化時,就會通知所有訂閱該漫畫的同學,是以漫畫也就是通知者。同學們收到通知後就可以去下載下傳漫畫的新篇章了,這就是主題對象會通知觀察者對象讓他們進行更新。

我們使用簡單的代碼,來嘗試實作一下這個場景:

漫畫類,也就是主題:

學生類,也就是觀察者:

用戶端:

運作結果:

以上的代碼實作了一個簡單的觀察者模式,當漫畫這個主題對象的狀态發生改變時,就會通知所有的訂閱者。

我們編寫的代碼雖然可以實作以上所說到的場景,但是代碼的耦合性很高,不能完全符合觀察模式的設計理念。例如,我要增加一個訂閱的是小說類型的學生,那麼就得去修改 “漫畫” 通知者的代碼了。如果我還要增加一個 “小說” 通知者,讓 小說” 通知者也能通知所有學生的話,也需要去修改學生類的代碼,這就不符合開-閉原則了,而且對象之間互相依賴也違背了依賴倒轉原則,以及以上的代碼中沒有編寫取消訂閱的方法也就是減少觀察者的方法。

既然知道代碼有哪些問題了,那麼我們就來把這些代碼重構一下:

代碼結構圖:

設計模式之觀察者模式

1.首先抽象兩個類:

2.然後才是具體的實作類:

漫畫類:

小說類:

訂閱漫畫的學生:

訂閱小說的學生:

用戶端代碼:

從用戶端的代碼可以看到,抽象了兩個類之後,即便是隻有一個 ”漫畫“ 通知者也能夠通知訂閱不同類型主題的觀察者,而不需要去修改任何的代碼。同樣的,我把 ”漫畫“ 通知者換成 “小說” 通知者也絲毫不會受到影響:

這樣的設計就滿足了依賴倒轉原則以及開-閉原則,算得上是一個完整的觀察者模式設計的代碼了。

監聽與通知示意圖:

設計模式之觀察者模式

我們再來看看觀察者模式(Observe)的結構圖:

設計模式之觀察者模式

我們來使用代碼實作這個結構:

Subject類。該類通常被稱為主題或抽象通知者,一般使用一個抽象類或者接口進行聲明。它把所有對觀察者對象的引用儲存在一個集合裡,每個主題都可以有任何數量的觀察者。抽象主題提供一個接口,可以增加和删除觀察者對象:

Observer類,抽象觀察者,為為所有的具體觀察者定義一個接口,在得到主題的通知時更新自己。這個接口叫更新接口。抽象觀察者一般用一個抽象類或者一個接口實作。更新接口通常包含一個update方法,這個方法叫更新方法:

ConcreteSubject類,叫做具體的主題或具體的通知者,該類将有關狀态存入具體的觀察者對象。在具體主題的内部狀态改變時,給所有登記過的觀察者發出通知。具體主題角色通常用一個具體的子類來進行實作:

ConcreteObserver類,具體的觀察者類,實作抽象觀察者角色所要求的更新接口,以便使本身的狀态與主題的狀态相協調。具體觀察者角色可以儲存一個指向具體主題對象的引用。具體觀察者角色通常用一個具體的子類進行實作:

觀察者模式特點:

将一個系統分割成一系列互相協作的類有一個很不好的副作用,那就是需要維護相關對象之間的一緻性。我們不希望為了維持一緻性而使各類緊密耦合,這樣會給維護、擴充和複用都帶來不便。而觀察者模式的關鍵對象是主題 Subject 和觀察者Observer ,一個 Subject 可以有任意數目的依賴它的 Observer ,一旦Subject的狀态發生了改變,所有的Observer 都可以得到通知。Subject發出通知時并不需要知道誰是它的觀察者,也就是說,具體觀察者是誰,它根本不需要知道。而任何一個具體觀察者不知道也不需要知道其他觀察者的存在,這樣降低了子類之間的耦合。

什麼時候考慮使用觀察者模式?

1.當一個對象的改變需要同時改變其他對象的時候,而且它不知道具體有多少個對象有待改變時,應該考慮使用觀察者模式

2.當一個抽象模型有兩個方面,其中一方面依賴于另一方面,這時用觀察者模式可以将這兩者封裝在獨立的對象中使它們各自獨立地改變和複用。

觀察者模式所做的事情其實就是解耦合,讓耦合的雙方都依賴于抽象,而不是依賴于具體,進而使得各自的變化都不會影響另一邊的變化。

觀察者模式的不足:

我們沒辦法讓每個控件都是實作一個 “Observer” 接口,因為這些控件都早已被它們的制造商封裝好了。而且我們上面的例子,盡管已經用了依賴倒轉原則,但是 “抽象通知者” 還是依賴 ”抽象觀察者“ ,也就是說,萬一沒有了 ”抽象觀察者“ 這樣的接口,那麼通知功能就無法完成了。既然 ”通知者“ 和 ”觀察者“ 之間根本就互相不知道,那麼我們就換另一種方式,讓用戶端來決定通知誰,這就是接下來要提到的事件委托模式。

事件委托模式在Java的Swing圖形化中經常使用,但是在Java語言中沒有對其做一定的封裝,是以實作起來沒那麼容易,不過反射機制學得還不錯的話,其實很好了解實作原理。相比之下C#就容易了很多,C#裡有一個delegate關鍵字,隻需要聲明一個委托器就可以了。在Java中我們需要自己通過反射機制去實作,正好把上面示範的例子使用事件委托模式進行重構,一會再說明什麼是事件委托:

設計模式之觀察者模式

1.去掉觀察者Observer接口,把兩個具體的觀察者類的代碼修改為如下内容:

2.定義一個事件類,該類通過反射機制完成對觀察者對象方法的調用:

3.事件處理類,該類将事件源資訊收集給事件類:

4.通知者接口:

5.具體的通知者:

Story類的代碼也是一樣的,忽略。

6.用戶端代碼:

以上用戶端的代碼可以看到,為了友善示範,我們是通過字元串來傳遞需要執行的方法的名稱。還有另一種方式就是可以通過接口去定義方法的名稱,就像Swing中添加點選事件一樣,需要實作ActionListener接口裡定義的actionPerformed方法,這樣我們就隻需要傳遞觀察者對象即可,然後反射機制就掃這個對象是否有實作接口中定義的方法就可以了。不過如果不是像點選事件那種固定不變的方法的話,還是使用字元串來傳遞需要執行的方法的名稱會好一些,這樣便于修改。

事件委托說明:

現在就可以來解釋一下,事件委托是什麼了。這就好比我是班長你是班主任,你讓我通知某幾個學生去辦公室,然後我就去通知那幾個學生辦公室,這就是一個委托,你委托的事情是讓我去通知你指定的那幾個學生。而我就是通知者,與觀察者模式不同的是,我是因為有你的委托才能去通知學生,而觀察者模式是當主題狀态發生變化時通知觀察者。上面的用戶端代碼裡,我們将訂閱了相關内容的學生,委托給了通知者,是以通知者就可以對這些學生發出通知,但實際調用觀察者方法的是Event類,不是通知者了。

而且一個委托可以搭載多個方法,這些方法可以是不同類的方法,當發送通知時所有的方法會被依次調用。這樣我們就不需要在通知者上用一個集合存儲觀察者了,增加、減少觀察者的方法也不需要編寫了,而是轉到用戶端來讓給委托搭載多個方法,這就解決了本來與抽象觀察者耦合的問題。也就是說觀察者模式是由抽象的觀察者來決定調用哪個方法,而事件委托模式是由用戶端決定調用哪個方法,這樣通知者就不需要依賴抽象觀察者了。

本文轉自 ZeroOne01 51CTO部落格,原文連結:http://blog.51cto.com/zero01/2066623,如需轉載請自行聯系原作者