觀察者模式筆記
OHYA,我又來了,今天開門見山的談談設計模式中的觀察者模式!
為什麼需要觀察者模式,我們以Blog更新RSS的範例為例
假如今天Blog 有RSS的更新訂閱,透過RSS中心統一收集Blog的RSS,並傳送
給有訂閱的Bloger。他們的關係如下圖:
這之間的關係為什麼要加一個Reader呢?因為有訂閱過RSS的都知道,RSS是用XML編碼形成,其實不太好直接閱讀,通常可以透過一些閱讀器幫你整理成結構化又簡潔的文章囉!
依循上述的需求,我們可能會想如何寫成一個這樣的關係的程式呢?
如何利用RSS更新資料的”物件”,取得資料並更新數個有訂閱的RSS Reader呢?(上面目前雖然只有畫一個,這邊可以假設有很多很多個訂閱者喲)。
這樣的需求可以同樣在真實世界中看到,例如:深入淺出所舉的生動實例:氣象站與氣象顯示裝置。氣象資料物件會透過氣象站取得資料,並更新氣象顯示狀置(天氣統計、天氣狀況等)。
本例子中,給Blog RSS更新資料 以及氣象資料物件一個最明白的需求,你必須讓你的物件知道現況是什麼。
在我們的情境中,我們為了進一步的模擬我們有數個Reader,假設目前有兩家最大的分別是Goooogle或yahooooo都出一款RSS Reader,每個Reader是Bloger用來訂閱Blog RSS的工具(我們這邊先不管,Reader如何與Blog綁定,先Focus在Blog是用Reader來訂閱顯示RSS的更新資料)。
那在我們的情境中,Blog可以選擇自己的RSS Reader,只是目前假設市面上有兩大家Rss Reader的軟體開發商,他們在介面的顯示上有一些些的不一樣
請記住,本觀察者模式注重在一對多的關係,這邊正是指一個Blog RSS中心跟所有的訂閱者透過Rss Reader顯示RSS的關係喲!
我們可能會這樣設計:
1: public class RssData
2: {
3: public void blogUpdate(){ //在資料有更新的時候呼叫getxxx方法取得最新資訊
5: int LatestPostID = getLatestPostID(); //最新RSS的文章編號
8: DateTime LatestPostDate =getLatestPostDate( ); //最新RSS的張貼文章時間
10: string LatestTitle = getLatestTitle(); //最新RSS的文章標題
12: string LatestPoster = getLatestPoster();//最新RSS的文章張貼人
16: string LatestType = getLatestType( ); //最新RSS的文章分類
19: //呼叫每個一個有訂閱的BlogerReader更新他們
20: GoooogleBlogerReader.Update(LatestPostID,LatestPostDate,
21: LatestTitle,LatestPoster,LatestType);
22:
23: yahoooooBlogerReader.Update(LatestPostID,LatestPostDate,
24: LatestTitle,LatestPoster,LatestType);
26: }
27: }
在上述的實踐版本中犯了什麼錯?
若有看上一篇策略模式的話,你會知道,我們若針對實踐寫程式,會導致我們未來在新增或刪除有訂閱的BlogerReader時,必須修改程式。我們無法在執行期動態地增加或刪除訂閱的BlogerReader,因為我們「尚末封裝會改變的部分」。
既然知道了問題,我們來看看如何透過觀察者模式來幫助這個需求的設計吧
簡單來說:出版者 + 訂閱者 = 觀察者模式 ,更仔細的來看:觀察者與主題將會是這樣的關係
好了好了,你可能會說,你說的我看的懂了,這是一對多的關係嘛,不過物件類別要怎麼設計才能包含所謂的彈性啊!?
沒錯:觀察者模式的定義就是:
“定義了物件之間一對多關係,如此一來,當一個物件改狀態,其他相依者都會收到通知並自動更新” --來自於”深入淺出設計模式”
讓我們定義一下最常見觀察者模式的類別圖(這也就是說實踐的方法不只一種,其他可以詳見Java SDK裡面某些Package的設計囉,請見原書”深入淺出”)
我們將看見鬆綁的威力,
這次一樣有一些OO的設計守則
觀察者模式所奉行的設計守則是:
“設計時,盡量讓需要互動的物件之間關係鬆綁”
這樣有哪些好處?
1.被觀察者(主題)對於觀察者,主題只知道觀察者有實踐特定的介面(觀察者介面)
如此一來,主題不需要知道觀察者的具體類別為何,做了些什麼跟細節囉
2.任何時候都可以加入新的觀察者。同樣的我們可以在執行期間動態地移除觀察者。
3.有新型態的觀察者出現的時候,主題的程式碼無須修改,只需要向動態地向主題註冊即可
4.我們可以在其他地方運用主題或觀察者,因為這兩者已經鬆綁了。
5.片面改變主題或觀察者,並不會影響另一方(前提是,兩者之間介面要遵守即可)
因為物件的相依度被降到最低,所以我們可以設計一個有彈性的OO系統囉(現學現賣!)
但是要如何實踐程式碼呢?還是不夠具體嗎?我們來看看我們如何實踐RSS訂閱機制
我們先定義最上層的結構 主題 V.S. 觀察者
以下因為前面說明時,類別圖已經先丟出來了,所以一些解說就在程式中的註解說明
1: //主題角色,記錄有哪些觀察者在收聽訊息
2: //在此代表BlogRss的主題角色
3: public interface BlogRSSCenter //play as Subject role 部落格更新時提供RSS的角色
4: {
5: void registerBloger(RssBloger b);
6: void removeBloger(RssBloger b);
7: void notifyBloger();
8: }
9:
10: public interface RssBloger //定義有訂閱RSS的部落客所使用的Reader有共同更新介面。
11: {
12: void update(int id, DateTime date, string title, string poster, string type);
13: }
14:
15:
16: public interface DisplayRSS //定義一下有訂閱RSS的部落客的Reader有共同顯示接口。
17: {
18: void display();
19: }
接著繼續RssData實踐主題介面(可對應著前面的類別圖來看程式碼:
1: public class RssData : BlogRSSCenter //rss的更新資料繼承Blog更新RSS中心所定義的方法,作為Subject的實踐
2: {
3: private List<RssBloger> RssBlogers;
4: private int LatestPostID; //最新RSS的文章編號-->方便部落格取得這個訊息後,透過QueryString查詢到Blog的post
5: private DateTime LatestPostDate; //最新RSS的張貼文章時間
6: private string LatestTitle; //最新RSS的文章標題
7: private string LatestPoster; //最新RSS的文章張貼人
8: private string LatestType; //最新RSS的文章分類
9:
10:
11: public RssData() //建構式時被建立用來記錄訂閱Rss的部落客
12: {
13: RssBlogers = new List<RssBloger>();
14: }
15:
16: public void registerBloger(RssBloger b) //當一個部落客要訂閱RSS資訊時的記錄行為
17: {
18: RssBlogers.Add(b);
19: }
20:
21: public void removeBloger(RssBloger b) //當一個部落客取消定閱RSS的時候
22: {
23: int i = RssBlogers.IndexOf(b);
24: if (i >= 0)
25: {
26: RssBlogers.RemoveAt(i);
27: }
28: }
29:
30: public void notifyBloger() //當Blog有新的RSS更新時,會呼叫這個方法以通知所有訂閱的部落客
31: {
32: foreach (var b in RssBlogers)
33: {
34: b.update(LatestPostID,LatestPostDate,LatestTitle,LatestPoster,LatestType);
35: }
36: }
37:
38: public void ArticlePosted() //當有新的Blog文章被張貼出來的時候就會通知所有部落客
39: {
40: notifyBloger();
41: }
42:
43: public void PostUpdated(int id, DateTime date, string title, string poster, string type)
44: {
45: //當一個文章被更新的方法,因為在本程式中沒有實作"整個"Blog,因此
46: //以這個function作為blog擷取張貼新的文章以作為測試囉!!
47: this.LatestPostID = id;
48: this.LatestPostDate = date;
49: this.LatestTitle = title;
50: this.LatestPoster = poster;
51: this.LatestType = type;
52: ArticlePosted();
53: }
54:
55:
56: }
接著部落客會跟某個blog的RSS中心去訂閱,並透過RSS Reader去訂閱Blog的RSS資訊。那麼就來實踐顯示的角色吧 ,如同前面情境所說,目前有兩家最大的分別是Goooogle或yahooooo都出一款RSS Reader,部落客透過這個reader去閱讀所訂閱的Blog RSS)
註again:本例中把blog rss reader簡化,請注意,Rss Reader等於是部落客去訂閱RSS而顯示的RSS佈告欄!在執行期可以動態的新增不同的使用者搭配不同的Reader。也請記住,本模式注重在一對多的關係,這邊正是指一個Blog RSS中心跟所有的訂閱者透過Rss Reader顯示RSS的關係!姑且不論RSS Reader如何去關係到多個bloger
我們分別實踐二家的Reader介面
1: //GoooogleBlogerReader的顯示規格是:最新標題、最新日期以及最新張貼ID
2: public class GoooogleBlogerReader: RssBloger , DisplayRSS
3: {
4: //
5: //實踐DisplayRSS在規定每個Rss Reader都要去實踐顯示最新RSS訊息(可以透過部落客所希望的方式呈現)
6: private int LatestPostID; //最新RSS的文章編號-->方便部落格取得這個訊息後,透過QueryString查詢到Blog的post
7: private DateTime LatestPostDate; //最新RSS的張貼文章時間
8: private string LatestTitle; //最新RSS的文章標題
9:
10: private RssData RssData;
11:
12: public GoooogleBlogerReader(RssData rssData)
13: {
14: this.RssData = rssData;
15: RssData.registerBloger(this);
16: }
17:
18: public void update(int id, DateTime date, string title, string poster, string type)
19: {
20: //gooooogle實作自己的reader資料只要顯示時間跟標題,其他的他想要用超連結連到blog上去看就好了
21: this.LatestPostID = id;
22: this.LatestPostDate = date;
23: this.LatestTitle = title;
24: display();
25: }
26:
27: public void display()
28: {
29: Console.WriteLine("Gooooogle:RSS更新:文章標題:{0} 張貼日期:{1} 張貼文章ID:{2}",
30: this.LatestTitle, this.LatestPostDate.ToString() , this.LatestPostID.ToString());
31: //真實的閱讀器可以再實作超連結連到文章啦
32: }
33: }
34:
35:
36: //正巧如上面所說,yahooooo也要加入閱讀器的戰爭,所以他也加入了設計一個閱讀器,讓bloger可以訂閱接收最新的rss資訊
37: //不過yahooooo的顯示格式有一點不一樣,資訊更豐富,以下實踐看看!
38: public class yahoooooBlogerReader : RssBloger, DisplayRSS
39: {
40: //
41: //實踐DisplayRSS在規定每個Rss Reader都要去實踐顯示最新RSS訊息(可以透過部落客所希望的方式呈現)
42: private int LatestPostID; //最新RSS的文章編號-->方便部落格取得這個訊息後,透過QueryString查詢到Blog的post
43: private DateTime LatestPostDate; //最新RSS的張貼文章時間
44: private string LatestTitle; //最新RSS的文章標題
45: private string LatestPoster; //最新RSS的文章張貼人
46: private string LatestType; //最新RSS的文章分類
47:
48: private RssData RssData;
49:
50: public yahoooooBlogerReader(RssData rssData)
51: {
52: this.RssData = rssData;
53: RssData.registerBloger(this);
54: }
55:
56: public void update(int id, DateTime date, string title, string poster, string type)
57: {
58: //yahoooooo實作自己的reader資料,而且想要比goooooogle更完整!讓使用者
59: //可以在閱讀器上讀取到更多的資料,例如張貼人...etc
60: this.LatestPostID = id;
61: this.LatestPostDate = date;
62: this.LatestTitle = title;
63: this.LatestPoster = poster;
64: this.LatestType = type;
65: display();
66: }
67:
68: public void display()
69: {
70: Console.WriteLine("Yahooooo:RSS更新:文章標題:{0} 張貼分類:{1} 張貼者:{2} 張貼日期:{3} 張貼文章ID:{4} \n\n",
71: this.LatestTitle,this.LatestType,this.LatestPoster, this.LatestPostDate.ToString(), this.LatestPostID.ToString()
72: );
73: //真實的閱讀器可以再實作超連結連到文章啦
74: }
75: }
該完成的都完成了,我們來啟動RSS更新中心吧(呼,期待中):
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: RssData rssData = new RssData();//blog網站可能會先建立一組更新的RSS資料物件
6:
7: //使用Goooooogle閱讀器的使用者A註冊了Blog訂閱資訊
8: GoooogleBlogerReader BlogerA_use_goooogleReader = new GoooogleBlogerReader(rssData);
9: //使用yahooooo閱讀器的使用者B也註冊了Blog訂閱資訊,而且假設他們訂閱的都是同一個blog網站
10: yahoooooBlogerReader BlogerB_use_yahoooooReader = new yahoooooBlogerReader(rssData);
11: //當然,以此例,你還可以增加更多的BlogerC、D、E....,來作為不同的觀察者
12: //而這些觀察者可以因此收到RSS的更新資訊。
13:
14: //該BLOG網站假如陸續有五篇文章被post
15: rssData.PostUpdated(1, DateTime.Today, "一平的第一篇文章", "一平", "個人心得");
16: rssData.PostUpdated(2, DateTime.Today, "ASP.NET的資安機制", "gipi", "技術分享");
17: rssData.PostUpdated(3, DateTime.Today, "gipi是管理大師", "一平", "個人心得");
18: rssData.PostUpdated(4, DateTime.Today, "six的linq筆記", "LittleSix", "技術分享");
19: rssData.PostUpdated(5, DateTime.Today, "一平的第二篇文章", "一平", "個人心得");
20: //註:請注意,這邊上述五篇文章都是透過subject去更新資料哦。
21: //看看結果會如何?觀察者們(Blog使用不同廠商的兩個reader,但都同時訂閱該blog網站)會自動收到資料嗎?
22:
23: Console.ReadKey();
24:
25:
26: }
哇!Suprise!現在只要bloger有張貼新的文章,blog中心就會將更新訊息傳給所有有訂閱的Bloger Reader了耶!大功告成!!
後記:
感謝深入淺出的故事性引導,上述的例子雖然不是書上提供,而是自己想的,但總是不盡然完善,不過透過例子衍伸出來學習,也呼應了舉一反三的學習成效,真的能讓我快速的進入狀況,總而言之,上述我們實踐了觀察者模式,提供了設計守則與類別圖關係,相信已經可以輕易的瞭解這個模式的精髓,若是還是很不清楚的話,可以自己實作這隻程式,並把這個程式的元件關係畫一畫,相信可以快速的學習到這個模式的精神。
呼!還有二十一個…(哈哈)