###### tags: `Code Sense` `Design-Pattern` # 發佈/訂閱模式與觀察者模式差異 近期與做IOT朋友聊天,提到MQTT協定,花了一點時間去認真看他最原始的模式去理解使用的情境。發現他原型是發佈/訂閱模式,訂閱這件事情會讓我直接想到觀察者模式。所以花了一點時間去理解他們兩者的差異並做點紀錄。大致分為幾個脈絡整理 - 1. 實作觀察者模式 - 2. 實作發佈/訂閱者模式 - 3. 比較不同處 - 4. 了解應用情境 - 5. 做個簡易總結 ## 一、情境實作兩個模式 之前有寫過關於[委派的文章](https://hackmd.io/@spyua/HyueY3Yzi),有用網紅文章狀態被訂閱的例子來講解觀察者模式。這邊沿用同一個情境來實作這兩個模式的差異性。 情境一樣以IG網紅訂閱為例子,物件定義如下 - Influencer類型代表IG網紅 - Follower類型代表訂閱者 情境是粉絲去訂閱網紅動態文章 ### a. 觀察者模式 在這模式下我們會的角色定義如下 - Influencer 被觀察者 - Follower 觀察者 我們設計被觀察者Influencer可直接實作dotnet IObservable的訂閱與解除訂閱實作,其實說破就是很簡單的List操作。而觀察者要觀察的則是他的文章狀態,當文章狀態改變時去通知被觀察者的List,實作面就是List輪巡。 #### Influencer ```csharp= public class Influencer : IObservable<string> { private List<IObserver<string>> observers = new List<IObserver<string>>(); private string status; public string Status { get { return status; } set { status = value; foreach (var observer in observers) { observer.OnNext(status); } } } public IDisposable Subscribe(IObserver<string> observer) { if (!observers.Contains(observer)) { observers.Add(observer); } return new Unsubscriber(observers, observer); } private class Unsubscriber : IDisposable { private List<IObserver<string>> _observers; private IObserver<string> _observer; public Unsubscriber(List<IObserver<string>> observers, IObserver<string> observer) { this._observers = observers; this._observer = observer; } public void Dispose() { if (_observer != null && _observers.Contains(_observer)) _observers.Remove(_observer); } } } ``` #### Follower 訂閱者這邊則實作IObserver,需實作OnNext、OnError與OnComplete三個方法。 ```csharp= public class Follower : IObserver<string> { private string name; public Follower(string name) { this.name = name; } public void OnNext(string value) { Console.WriteLine($"{name} received an update: {value}"); } public void OnError(Exception error) { Console.WriteLine($"{name} noticed an error: {error.Message}"); } public void OnCompleted() { Console.WriteLine($"{name} has completed following."); } } ``` 如果有用過偏向數據變動觀察ReactiveX套件,對這三個方法就不會很陌生。如果沒有,大致講一下這三種方法的設計原理,一般會用在數據流的應用 - OnNext(T value):用來處理正常的值。例如,當觀察了一個網紅動態,每當他更新了新的狀態,就會通過OnNext方法收到通知,並且可以進行相應的處理(如查看新的狀態等)。 - OnError(Exception error): 用於處理在訂閱過程中出現的異常。比如,如果在嘗試獲取網紅的動態時發生了網路異常,你可以通過OnError方法獲得通知,並且可以進行相應的處理(如重新嘗試獲取狀態,或者顯示錯誤信息等)。 - OnCompleted():用來表示通知已經完成,數據串流結束。例如,如果一個網紅動態通知完畢後,你可以通過OnCompleted方法獲得通知,並且可以進行相應的處理。 這三種方式設計主要是為了提供一個更具有表達力和靈活性的數據流模型方法,數據在走訪時會有的三種情境,簡單來說就是繼續往下走、走到一半發生問題與走訪完成三種事件。 #### Context 以下是Context實際使用 ```csharp= public class Program { static void Main(string[] args) { Influencer influencer = new Influencer(); Follower follower1 = new Follower("Follower1"); influencer.Subscribe(follower1); Follower follower2 = new Follower("Follower2"); influencer.Subscribe(follower2); // Update status influencer.Status = "New status update!"; } } ``` #### 初步解說 使用範例可看到,追隨者訂閱網紅動態後,及會加入網紅物件的List資料欄位,當網紅動態發生變動時,會開始走訪訂閱者列表,透過OnNext讓數據走訪(通知)過每一個觀察者。這邊稍微注意一下,我們可以看到幾件事情 - 1. 觀察者與被觀察者兩個是耦合關係 - 2. 對於觀察者來說,觀察者的主要關注於一件事,但大多狀況為一個變化量 簡單來說以觀察者模式來看,存在於被觀察者觀察者兩個角色,觀察者會去註冊(加入List)被觀察者的某個狀態(數值)。當此數值有異動時,就會開始做數據流動(通知)傳送給每個觀察者。 ### b.發布/訂閱模式 在這模式下我們會的角色定義如下 - Influencer 發佈者 - Follower 訂閱者 #### Influencer 實作面我們將使用觀察者介面換成單純的委派實作,發佈訂閱模式通常著重於事件變化通知,所以網紅動態變動為例的話,我們這邊需要宣告一個文章變動事件,我們用Action去實作如下。 ```csharp= public class Influencer { // Define an event using Action<T> public event Action<string> StatusUpdated; private string status; public string Status { get { return status; } set { status = value; StatusUpdated?.Invoke(status); } } } ``` 雖然在發佈者上看不到List,但Action本身就是一個觀察者模式的包裝,這邊不再多做敘述怕搞混,你可以簡單把Action看成就是一個List方法封裝。使用上我們可以去加入一個輸入string參數格式的方法。他會以+=表示加入,-=表示移除,有點類同於加入與移除訂閱者想被通知的方法實作。 #### Follower 訂閱者部分,需先實作被通知的方法實作,以發佈者格式來看,它必須是一個單一string輸入的方法,因為訂閱者是訂閱網紅的動態是否變動,所以我們可以以Influencer_StatusUpdated去命名,而輸入參數就是網紅動態的新狀態。另外就是封裝訂閱方法,這邊我們宣告一個Follow方法,帶入發佈者物件,在裡面去實作加入文章變動方法。其實封裝這一個方法沒特別意義,就只是想呈現在Context應用上,程式描述可以有一種紛絲訂閱網紅的描述語句,看起來比較直覺。Ex follower.Follow(influencer); ```csharp= public class Follower { private string name; public Follower(string name) { this.name = name; } public void Follow(Influencer influencer) { influencer.StatusUpdated += Influencer_StatusUpdated; } private void Influencer_StatusUpdated(string newStatus) { Console.WriteLine($"{name} received an update: {newStatus}"); } } ``` #### Context ```csharp= public class Program { static void Main(string[] args) { Influencer influencer = new Influencer(); Follower follower1 = new Follower("Follower1"); follower1.Follow(influencer); Follower follower2 = new Follower("Follower2"); follower2.Follow(influencer); // Update status influencer.Status = "New status update!"; } } ``` #### 初步解說 其實我們以實際應用面來看,他跟觀察者非常像,差別在於Status改變實,觀察者是以流動的方式去走訪每一個觀察者實作函式(onNext),而發佈訂閱模式則是以事件通知方式去調用通知所有訂閱者註冊的訂閱方法。 ## 二、模擬良可 其實你看完上述例子,你還是會覺得觀察者與發佈/訂閱模式非常相似,有種硬套網紅動態內容例子去區分。一開始我看完也有種感覺。以下稍微簡單講一下差異性。 在觀察者模式中,觀察者通常關注被觀察者的內部變化。大多狀況會是一個數值,當數值改變時,會調用觀察者的某個方法(如、OnNext)來通知變化。所以觀察者與被觀察者他們之前會是依賴關係,通常用在單一個應用程式情境。你最常見的應用就是MVVM的Data Binding,當數值變化會主動通知View去修改顯示數值,或是你有用過ReactiveX,在一個程式下做非同步流程串接時,他的原理就是觀察者模式,當動作做完後得到一個數值,接著你會抓取這個數值在往下一個流程走(有興趣可以看一下ReactiveX,他是一個非常強大的非同步工具)。看到這邊,其實某種程度可以把觀察者歸類在單體執行程式數據流的應用上,當被觀察者產生數值變化時,這個數值變化的結果會往訂閱者方向流動(通知),過程當中有可能會發生錯誤,此時就會使用onError實作,或是數據流動完畢,此時就會使用onComplete實作。所以以上述網紅例子,數據這件事情以例子對應來說就是網紅的動態內容。 而在這個例子中,你會發現以發佈訂閱模式實作上發佈者與訂閱者也是相依關係,其實以單個程式執行架構來看確實是這樣。觀察者模式會與發佈/訂閱模式很多時候非常相似。但其實大多應用上,訂閱/發佈模式會應用於更大的分布系統,例如MQTT、Kafka架構的系統。在這樣的系統,訂閱者與發佈者之間會存在於一個中間層,通常稱為Broker > Broker,如事件通道或消息經紀人 那接下來我將Broker的概念設計進網紅動態被訂閱的例子,此時角色會有 - 1. EventBroker 中間層 - 2. Influencer 發佈者 - 3. Follower 訂閱者 EventBroker用於實作發佈、訂閱與解除訂閱方法,在訂閱者的管理上,以上述例子我們可以使用字典去紀錄訂閱者對哪一個網紅有興趣。因為它是一個中間層管理,所以他需要去紀錄哪個網紅動態變動時,要去通知哪一些訂閱者,所以這邊我們使用Dictionary資料格式去記錄這件事情。而訂閱取消訂閱時做,這邊我們以主題topic去命名,這邊的topic你可以視為就是網紅名稱 ```csharp= public class EventBroker { private Dictionary<string, Action<string>> subscribers = new Dictionary<string, Action<string>>(); public void Publish(string topic, string message) { if (subscribers.ContainsKey(topic)) { subscribers[topic]?.Invoke(message); } } public void Subscribe(string topic, Action<string> handler) { if (!subscribers.ContainsKey(topic)) { subscribers[topic] = null; } subscribers[topic] += handler; } public void Unsubscribe(string topic, Action<string> handler) { if (subscribers.ContainsKey(topic)) { subscribers[topic] -= handler; } } } ``` Broker實作完,接著來實作發發佈者Influencer,對發佈者來說最重要的就是發佈這件事情,訂閱者不會跟發佈者直接做訂閱,會透過broker去做訂閱,所以這邊不用特別實作訂閱與解除訂閱方法。需實作有下述幾點 - 1. 儲存狀態:在例子中,Influencer需要儲存狀態(這邊是status欄位)。當Influencer有新的狀態更新時,例如發佈新的內容,這個狀態就會被改變。 - 2. 更新狀態:Influencer擁有一個UpdateStatus的方法,可以用來更新其狀態。這個方法除了會更新Influencer內部的狀態欄位外,還會透過EventBroker將這個新的狀態公佈給所有訂閱了此Influencer的Follower。 - 3. 與EventBroker溝通:Influencer需要透過EventBroker來發佈新的狀態。在此例中,Influencer在構造器中接收了一個EventBroker的實例,並且在UpdateStatus方法中,使用這個EventBroker來發佈新的狀態。 ```csharp= public class Influencer { private EventBroker broker; private string name; private string status; public Influencer(EventBroker broker, string name) { this.broker = broker; this.name = name; } public string Name { get { return this.name; } } public string Status { get { return this.status; } } public void UpdateStatus(string status) { this.status = status; // 狀態更新 broker.Publish(name, status); // 將新的狀態發布出去 } } ``` 在Follower發佈者實作中,需參考EventBroker,去訂閱網紅的動態。實作面幾點有 - 1. 接收更新:當Influencer的狀態有變化時,Follower需要能接收到這個更新。通過OnStatusUpdate方法實現。這個方法會在Influencer的狀態有變化時被EventBroker調用。 - 2. 訂閱Influencer:Follower需要能夠訂閱某個Influencer,從而能接收到Influencer的狀態更新。在此例中,這是通過SubscribeTo方法實現的。該方法接收一個主題(在此例中是Influencer的名字)和一個EventBroker的實例,並在EventBroker上訂閱該主題。 ```csharp= public class Follower { private string name; public Follower(string name) { this.name = name; } public void OnStatusUpdate(string status) { Console.WriteLine($"{name} received an update: {status}"); } public void SubscribeTo(string topic, EventBroker broker) { broker.Subscribe(topic, OnStatusUpdate); } } ``` 情境如下,可以很明顯看到發佈與訂閱者因為Broker的存在而解偶了,這在單一個執行程式上效果不大。但如果是一個系統等級的架構,發佈者、Broker與訂閱者這三方就可以以一個獨立的程式服務運行互相協調溝通,實現系統等級的設計。例如啟一個MQTT服務,最少會有三個服務組成,這邊我以物聯網設備應用為例子 - 發布者(Publisher):信息的源頭,負責生成信息。假設你有一個感應器,例如溫度感應器,這個感應器定期讀取環境溫度並產生數據,然後將這些數據發送到MQTT代理。在這裡,溫度感應器就是發布者。 - 代理(Broker):信息的轉運站,溫度感應器(發布者)將讀取的溫度數據發送到MQTT代理。MQTT代理的工作就是接收這些數據,然後將這些數據傳遞給訂閱了相應主題的訂閱者。 - 訂閱者(Subscriber):信息的消費者,也就是需要信息的實體。比如,你有一個控制空調的應用程式,該應用程式需要知道當前的溫度來調整空調的運行狀態。這個應用程式可以訂閱MQTT代理中關於溫度的主題,一旦有新的溫度數據發布,MQTT代理就會將這些數據傳遞給這個應用程式(訂閱者)。 稍微提醒一下這三個角色,不一定是要三支獨立程式,簡單的架構為一個Broker程式與一個Publisher和Subscriber程式就可以做簡單應用了。 ```csharp= public static class Program { public static void Main() { // Create an event broker EventBroker broker = new EventBroker(); // Create an influencer Influencer influencer = new Influencer(broker, "Influencer1"); // Create followers Follower follower1 = new Follower("Follower1"); Follower follower2 = new Follower("Follower2"); // Subscribe the followers to the influencer follower1.SubscribeTo(influencer.Name, broker); follower2.SubscribeTo(influencer.Name, broker); // Update the status of the influencer influencer.UpdateStatus("New post!"); } } ``` ## 三、總結整理 稍微根據四個層面做個整理 - 關注點: - 觀察者模式:觀察者通常關注一個特定的主題或主題的一個具體實例。例如,一個觀察者可能專注於追蹤一個網紅的動態文內容變動。 - 發佈/訂閱模式:訂閱者通常關注一類事件,而不只是一個具體的實例。例如,一個訂閱者可能訂閱不同網紅文章變動相關的事件,而不僅僅是特定的網紅。 - 與主題的關聯性: - 觀察者模式:觀察者直接與主題相關聯,並從主題接收更新。這種連接通常是靜態的,並在主題和觀察者的生命周期內持續存在。 - 發佈/訂閱模式:訂閱者和發佈者之間通常存在一個或多個中間件(例如事件通道或消息隊列)。訂閱者通過這些中間件訂閱事件,而發佈者則透過它們發佈事件。這種連接可能更加動態,可以在任何時間點增加或刪除訂閱。 - 耦合性: - 在觀察者模式:觀察者和主題之間的耦合性可能較高,因為觀察者需要知道主題以便直接從其接收更新。 - 發佈/訂閱模式:由於存在中間件,所以發佈者和訂閱者之間的耦合性可能較低。 - 應用 : - 觀察者模式:單App執行程式,偏向數據變動觀察,並做數據流串接或處理。換句話說,觀察者模式允許數據的變動(也可以被視為一種事件)在對象之間傳播,而不需要這些對象相互了解。這種模式有助於保持應用程序的組件解耦,並且讓數據的變動可以在多個對象之間有效地分發和處理。 - 發佈/訂閱模式:大型系統服務架構,具有一對多服務訂閱通知應用場景,例如,一個服務可以發佈一個事件,而多個其他服務可以訂閱該事件並對其作出反應。