訂閱者模式在生活中處處可見,例如讀者訂閱新聞。而我是這麼理解觀察者模式的;當我在意的「新聞中心」有更新時,它會通知我,我再去看它的更新為何,在參考書中通常會將新聞中心這個角色稱為「主題」,讀者稱為「觀察者」,下面的範例就會以讀者訂閱新聞去做解釋。主要會分為三段:
    - 主題
 
  - 觀察者
 
  - 註冊及發送通知
 
主題 (Subject)
首先先來定義「新聞中心」會做什麼事,假設它有以下的三個功能:
    - 接受讀者訂閱
 
  - 增加新聞
 
  - 通知讀者
 
我們將這三個功能定義成一個介面,然後簡單定義新聞類別,最後就是實作新聞中心:
ISubject.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | 
  namespace DemoCode.DesignPattern.Observer {     public interface ISubject     {                                             void RegisterObserver(IObserver observer);
                                     void NotifyObservers();
                                              void AddNews(News news);     } }
 
  | 
 
NewsCenter.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
   | 
  using System.Collections.Generic;
  namespace DemoCode.DesignPattern.Observer {     public class NewsCenter : ISubject     {                                    private List<IObserver> _itsObservers;
                                     public List<News> _newsList;
          public NewsCenter()         {             this._itsObservers = new List<IObserver>();             ``this._newsList = new List<News>();         }
                                     public void NotifyObservers()         {             foreach(var observer in this._itsObservers)             {                 observer.Update();             }         }
                                              public void RegisterObserver(IObserver observer)         {             this._itsObservers.Add(observer);         }
                                              public void AddNews(News news)         {             this._newsList.Add(news);             this.NotifyObservers();         }     } }
 
  | 
 
News.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | 
  namespace DemoCode.DesignPattern.Observer {                    public class News     {                                    public CategoryEnum Category { get; set; }
                                     public string Author { get; set; }
                                     public string Title { get; set; }
                                     public string Content { get; set; }     } }
 
  | 
 
CategoryEnum.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | 
  namespace DemoCode.DesignPattern.Observer {                    public enum CategoryEnum     {                                    Politics,
                                     Sports     } }
 
  | 
 
觀察者 (Observer)
現實生活中,通常是讀者收到新聞更新的通知後,再決定要做什麼,我們先簡單定義讀者只會做 Update() 這個動作
IObserver.cs
1 2 3 4 5 6 7 8 9 10 11 12
   | 
  namespace DemoCode.DesignPattern.Observer {     public interface IObserver     {                                    void Update();     } }
 
  | 
 
Reader.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
   | 
  using System; namespace DemoCode.DesignPattern.Observer {     public class Reader : IObserver     {                                    private string _readerName;
                                     private CategoryEnum _registeredCategory;
                                                       public Reader(string readerName, CategoryEnum category)         {             this._readerName = readerName;             this._registeredCategory = category;
              this.RegisterMessage(readerName, category);         }
                                     public void Update()         {                 Console.WriteLine($"{this._readerName} 訂閱的新聞中心有新的新聞,但不知道新聞類型");         }
                                                       private void RegisterMessage(string readerName, CategoryEnum category)         {             Console.WriteLine($"{readerName} 訂閱了新聞類型:{category}");         }     } }
 
  | 
 
註冊及發送通知
最後我們來建立「新聞中心」及「讀者」,並新增幾則新聞看看結果:
Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
   | 
  using System; using DemoCode.DesignPattern.Factory; using DemoCode.DesignPattern.Observer;
  namespace DemoCode {     class Program     {         static void Main(string[] args)         {                          ISubject newsCenter = new NewsCenter();
              IObserver reader1 = new Reader("Jack", CategoryEnum.Politics);             IObserver reader2 = new Reader("Lily", CategoryEnum.Sports);
              newsCenter.RegisterObserver(reader1);             newsCenter.RegisterObserver(reader2);
              News news1 = new News(){ Category = CategoryEnum.Sports, Author = "Leo", Title = "2021東京奧運" };             News news2 = new News() { Category = CategoryEnum.Politics, Author = "Jerry", Title = "莫德納疫苗抵台" };
              newsCenter.AddNews(news1);             newsCenter.AddNews(news2);         }     } }
 
  | 
 
拉模型與推模型
拉模型 (pull-model)
以上的範例我們可以得知,讀者只收到新聞中心通知說有新的新聞,但不知道新聞類型為何,收到通知後讀者需要去「拉」 newsList 才知道該新聞為何,所以此類型稱為拉模型 (pull-model)。
推模型 (push-model)
如果只想在收到特定新聞類型時才做相對應的事情,可以在 NotifyObservers() 時多傳入一個參數,讓訂閱者根據傳入參數決定要做什麼事,這種傳入參數的方式即為推模型 (push-model)。Github 的範例即為以推模型做示範,並以 NotifyObservers(CategoryEnum category) 通知讀者新增的新聞類型為何。
完整程式碼請參考
結論
觀察者模式解決了「主題」更新後通知多個「觀察者」,且因為使用介面解了「主題」及「觀察者」間的耦合,每個訂閱者可以在收到通知後決定後續的動作。簡單介紹了拉模型與推模型,決定該使用何者取決於使用情境。
參考
OBSERVER 模式.無瑕的程式碼 敏捷完整篇:物件導向原則、設計模式與 C# 實踐
[Design Pattern] 觀察者模式 (Observer Pattern) 我也能夠辦報社