訂閱者模式在生活中處處可見,例如讀者訂閱新聞。而我是這麼理解觀察者模式的;當我在意的「新聞中心」有更新時,它會通知我,我再去看它的更新為何,在參考書中通常會將新聞中心這個角色稱為「主題」,讀者稱為「觀察者」,下面的範例就會以讀者訂閱新聞去做解釋。主要會分為三段:
- 主題
- 觀察者
- 註冊及發送通知
主題 (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) 我也能夠辦報社