觀察者模式

觀察者模式筆記

OHYA,我又來了,今天開門見山的談談設計模式中的觀察者模式!

 

為什麼需要觀察者模式,我們以Blog更新RSS的範例為例

 

假如今天Blog 有RSS的更新訂閱,透過RSS中心統一收集Blog的RSS,並傳送

給有訂閱的Bloger。他們的關係如下圖:

Pic0

這之間的關係為什麼要加一個Reader呢?因為有訂閱過RSS的都知道,RSS是用XML編碼形成,其實不太好直接閱讀,通常可以透過一些閱讀器幫你整理成結構化又簡潔的文章囉!

 

依循上述的需求,我們可能會想如何寫成一個這樣的關係的程式呢?

如何利用RSS更新資料的”物件”,取得資料並更新數個有訂閱的RSS Reader呢?(上面目前雖然只有畫一個,這邊可以假設有很多很多個訂閱者喲)。

這樣的需求可以同樣在真實世界中看到,例如:深入淺出所舉的生動實例:氣象站與氣象顯示裝置。氣象資料物件會透過氣象站取得資料,並更新氣象顯示狀置(天氣統計、天氣狀況等)。

本例子中,給Blog RSS更新資料 以及氣象資料物件一個最明白的需求,你必須讓你的物件知道現況是什麼。

 

在我們的情境中,我們為了進一步的模擬我們有數個Reader,假設目前有兩家最大的分別是Goooogleyahooooo都出一款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,因為我們「尚末封裝會改變的部分」。

 

既然知道了問題,我們來看看如何透過觀察者模式來幫助這個需求的設計吧

簡單來說:出版者 + 訂閱者 = 觀察者模式 ,更仔細的來看:觀察者與主題將會是這樣的關係

Pic2

好了好了,你可能會說,你說的我看的懂了,這是一對多的關係嘛,不過物件類別要怎麼設計才能包含所謂的彈性啊!?

 

沒錯:觀察者模式的定義就是:

“定義了物件之間一對多關係,如此一來,當一個物件改狀態,其他相依者都會收到通知並自動更新” --來自於”深入淺出設計模式”

讓我們定義一下最常見觀察者模式的類別圖(這也就是說實踐的方法不只一種,其他可以詳見Java SDK裡面某些Package的設計囉,請見原書”深入淺出”)

 

Pic1

我們將看見鬆綁的威力,

這次一樣有一些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:         }

 

Pic3

哇!Suprise!現在只要bloger有張貼新的文章,blog中心就會將更新訊息傳給所有有訂閱的Bloger Reader了耶!大功告成!!

 

後記:

感謝深入淺出的故事性引導,上述的例子雖然不是書上提供,而是自己想的,但總是不盡然完善,不過透過例子衍伸出來學習,也呼應了舉一反三的學習成效,真的能讓我快速的進入狀況,總而言之,上述我們實踐了觀察者模式,提供了設計守則與類別圖關係,相信已經可以輕易的瞭解這個模式的精髓,若是還是很不清楚的話,可以自己實作這隻程式,並把這個程式的元件關係畫一畫,相信可以快速的學習到這個模式的精神。

呼!還有二十一個…(哈哈)