# 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

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/)
---
## 新手上路,若有錯誤還請告知,謝謝