# 20. Observer ###### tags: `DesignPatterns` ## 關於 Observer 本篇將討論以下幾個問題 > ### 1. 關於 Observer > ### 2. UML > ### 3. 將 UML 轉為程式碼 > ### 4. 情境 --- ## 測試環境: >OS:Windows 10 >IDE:Visual Studio 2019 --- ## 1. 關於 Observer > Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. > > by [Gang of Four](https://en.wikipedia.org/wiki/Design_Patterns) - 在物件間定義一對多依賴關係,當一個物件改變狀態時,所有依賴關係都會被通知並自動更新 Observer(觀察者)屬於行為型(Behavioral Patterns),當遇到**自身狀態異動就會有其他物件要跟著處理**時,可藉由 Observer 來處理通知邏輯,將「狀態異動的物件」與「要跟著處理的物件」解除耦合,之後新增一個「要跟著處理的物件」時,直接訂閱通知即可。 優點: - 符合 開閉原則(Open Closed Principle) 缺點: - 維護上較不易,錯誤的修改可能造成訂閱者收到非預期通知卻不易察覺 --- ## 2. UML ![](https://i.imgur.com/YxY76pe.jpg) Class 間關聯: - Subject 關聯 Observer - ConcreteSubject 繼承 Subject - ConcreteObserver 繼承 Observer - ConcreteObserver 關聯 ConcreteSubject Class: - Subject: - ConcreteSubject: - Observer: - ConcreteObserver: --- ## 3. 將 UML 轉為程式碼 Subject 的抽象類別,實作加入 & 移除觀察者 ```C# /// <summary> /// Subject 的抽象類別,實作加入 & 移除觀察者 /// </summary> public abstract class Subject { private List<IObserver> _observers = new List<IObserver>(); public void Attach(IObserver observer) { _observers.Add(observer); } public void Detach(IObserver observer) { _observers.Remove(observer); } public void Notify() { foreach (IObserver observer in _observers) { observer.Update(); } } } ``` Subject 實作 ```C# /// <summary> /// Subject 實作 /// </summary> public class ConcreteSubject : Subject { public string SubjectState { get; set; } } ``` 觀察者介面 ```C# /// <summary> /// 觀察者介面 /// </summary> public interface IObserver { void Update(); } ``` 觀察者實作 ```C# /// <summary> /// 觀察者實作 /// </summary> public class ConcreteObserver : IObserver { public ConcreteSubject Subject { get; set; } private string _name; private string _observerState; public ConcreteObserver(ConcreteSubject subject, string name) { Subject = subject; _name = name; } public void Update() { _observerState = Subject.SubjectState; Console.WriteLine($"Observer {_name}'s new state is {_observerState}"); } } ``` 1. 建立`originator`並給定初始值 On 2. 建立`caretaker`並透過`originator`建立`Memento` 3. 改變`State`的值 4. 透過`Memento`還原`State`的值 ```C# static void Main(string[] args) { Default.ConcreteSubject subject = new Default.ConcreteSubject(); subject.Attach(new Default.ConcreteObserver(subject, "X")); subject.Attach(new Default.ConcreteObserver(subject, "Y")); subject.Attach(new Default.ConcreteObserver(subject, "Z")); subject.SubjectState = "ABC"; subject.Notify(); Console.ReadLine(); } ``` 執行結果 ```Console Observer X's new state is ABC Observer Y's new state is ABC Observer Z's new state is ABC ``` --- ## 4. 情境 我們接到了一個缺貨商品到貨通知的需求 - 商品到貨狀態改變時通知有點選到貨通知的客戶 到貨狀態 enum ```C# public enum StockType { OutOfStock, InStock } ``` 商品抽象類別,實作加入 & 移除觀察者 ```C# /// <summary> /// 商品抽象類別,實作加入 & 移除觀察者 /// </summary> public abstract class Product { private List<IObserver> _observers = new List<IObserver>(); public void Attach(IObserver observer) { _observers.Add(observer); } public void Detach(IObserver observer) { _observers.Remove(observer); } public void Notify() { foreach (IObserver observer in _observers) { observer.Update(); } } } ``` 商品介面,紀錄商品名稱 & 到貨狀態 ```C# public interface IProduct { string Name { get; } StockType StockType { get; set; } } ``` 商品實作 ```C# /// <summary> /// 商品實作 /// </summary> public class AppleProduct : Product, IProduct { public string Name { get; } public StockType StockType { get; set; } public AppleProduct(string name) { Name = name; } } ``` 觀察者介面 ```C# /// <summary> /// 觀察者介面 /// </summary> public interface IObserver { void Update(); } ``` 觀察者實作 ```C# /// <summary> /// 觀察者實作 /// </summary> public class ConcreteObserver<T> : IObserver where T : IProduct { private T Product { get; } private string _customerName; private StockType _stockType; public ConcreteObserver(T product, string customerName) { Product = product; _customerName = customerName; } public void Update() { _stockType = Product.StockType; Console.WriteLine($"Hi {_customerName}, {Product.Name} is {_stockType}."); } } ``` 1. 建立商品`iPhone` & `iPad` 2. 將商品狀態設為`StockType.OutOfStock` 3. 客戶點選`iPhone` & `iPad`到貨通知 4. 商品狀態改變,通知客戶 ```C# static void Main(string[] args) { Situation.AppleProduct iPhone = new Situation.AppleProduct("iPhone"); iPhone.StockType = Situation.StockType.OutOfStock; Situation.AppleProduct iPad = new Situation.AppleProduct("iPad"); iPad.StockType = Situation.StockType.OutOfStock; iPhone.Attach(new Situation.ConcreteObserver<Situation.AppleProduct>(iPhone, "John")); iPhone.Attach(new Situation.ConcreteObserver<Situation.AppleProduct>(iPhone, "Wayne")); iPad.Attach(new Situation.ConcreteObserver<Situation.AppleProduct>(iPad, "Leo")); iPad.Attach(new Situation.ConcreteObserver<Situation.AppleProduct>(iPad, "Henry")); iPhone.StockType = Situation.StockType.InStock; iPad.StockType = Situation.StockType.InStock; iPhone.Notify(); iPad.Notify(); Console.ReadLine(); } ``` 執行結果 ```Console Hi John, iPhone is InStock. Hi Wayne, iPhone is InStock. Hi Leo, iPad is InStock. Hi Henry, iPad is InStock. ``` --- ## 完整程式碼 GitHub:[Behavioral_07_Observer](https://github.com/darionnnnnn/blog/tree/master/Blog/Behavioral_07_Observer) --- ## 總結 ### Observer 通知訂閱者通常屬於射後不理的類型,也許會考慮使用開新的 Task 或是 Thread 來處理,但我們並不知道訂閱者的數量,若讓程式無限制的開新的 Task 或是 Thread 則可能會影響到主要系統的運行,且此問題通常在測試時不易發現,要特別留心。 --- ## 參考資料 1. [Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns) 2. [大話設計模式](https://www.tenlong.com.tw/products/9789866761799) 2. [dofactory](https://www.dofactory.com/) 3. [Refactoring.Guru](https://refactoring.guru/) --- ## 新手上路,若有錯誤還請告知,謝謝