20. Observer


Posted by WayneCheng on 2021-01-27

關於 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

  • 在物件間定義一對多依賴關係,當一個物件改變狀態時,所有依賴關係都會被通知並自動更新

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 的抽象類別,實作加入 & 移除觀察者

/// <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 實作

/// <summary>
/// Subject 實作
/// </summary>
public class ConcreteSubject : Subject
{
    public string SubjectState { get; set; }
}

觀察者介面

/// <summary>
/// 觀察者介面
/// </summary>
public interface IObserver
{
    void Update();
}

觀察者實作

/// <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的值
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();
}

執行結果

Observer X's new state is ABC
Observer Y's new state is ABC
Observer Z's new state is ABC

4. 情境

我們接到了一個缺貨商品到貨通知的需求

  • 商品到貨狀態改變時通知有點選到貨通知的客戶

到貨狀態 enum

public enum StockType
{
    OutOfStock,
    InStock
}

商品抽象類別,實作加入 & 移除觀察者

/// <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();
        }
    }
}

商品介面,紀錄商品名稱 & 到貨狀態

public interface IProduct
{
    string Name { get; }
    StockType StockType { get; set; }
}

商品實作

/// <summary>
/// 商品實作
/// </summary>
public class AppleProduct : Product, IProduct
{
    public string Name { get; }
    public StockType StockType { get; set; }

    public AppleProduct(string name)
    {
        Name = name;
    }
}

觀察者介面

/// <summary>
/// 觀察者介面
/// </summary>
public interface IObserver
{
    void Update();
}

觀察者實作

/// <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. 商品狀態改變,通知客戶
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();
}

執行結果

Hi John, iPhone is InStock.
Hi Wayne, iPhone is InStock.
Hi Leo, iPad is InStock.
Hi Henry, iPad is InStock.

完整程式碼

GitHub:Behavioral_07_Observer


總結

Observer 通知訂閱者通常屬於射後不理的類型,也許會考慮使用開新的 Task 或是 Thread 來處理,但我們並不知道訂閱者的數量,若讓程式無限制的開新的 Task 或是 Thread 則可能會影響到主要系統的運行,且此問題通常在測試時不易發現,要特別留心。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

F2E合作社|在HTML中設置錨點的三種方式

F2E合作社|在HTML中設置錨點的三種方式

【Day05】Youtube GUI 下載程式

【Day05】Youtube GUI 下載程式

基礎格線觀念 筆記

基礎格線觀念 筆記


Comments