引子:老闆回來,我不知道#

小菜公司同事們上班偷看股票行情,老闆出門時請前台秘書童子喆當「眼線」——一旦老闆回辦公室就打電話通知,大家好各就各位。

但這天老闆回來時順手把童子喆叫去印文件,沒人來得及打電話。背對門的魏關巡還大喊「我的股票漲停了哦」,回頭就看到老闆憤怒的臉。

一個訊息要通知多個對象——這就是觀察者模式的典型場景。

第一版:雙向耦合#

class Secretary
{
    private IList<StockObserver> observers = new List<StockObserver>();
    private string action;

    public void Attach(StockObserver observer) => observers.Add(observer);
    public void Notify()
    {
        foreach (var o in observers) o.Update();
    }
    public string SecretaryAction { get => action; set => action = value; }
}

class StockObserver
{
    private string name;
    private Secretary sub;
    public StockObserver(string name, Secretary sub) { this.name = name; this.sub = sub; }
    public void Update()
    {
        Console.WriteLine($"{sub.SecretaryAction} {name} 關閉股票行情,繼續工作!");
    }
}

SecretaryStockObserver 互相耦合

  • 若新增「看 NBA 的同事」,要改 Secretary
  • 若把通知者從前台改成老闆本人,又要再改

違反了開放-封閉原則依賴倒轉原則

第二版:抽象觀察者#

abstract class Observer
{
    protected string name;
    protected Secretary sub;
    public Observer(string name, Secretary sub) { this.name = name; this.sub = sub; }
    public abstract void Update();
}

class StockObserver : Observer { /* ... */ }
class NBAObserver   : Observer { /* ... */ }

還是不夠。觀察者依然依賴具體的 Secretary 類別。如果通知者換成老闆本人呢?

第三版:抽象通知者#

進一步把通知者也抽象出來:

interface Subject
{
    void Attach(Observer observer);
    void Detach(Observer observer);
    void Notify();
    string SubjectState { get; set; }
}

class Boss : Subject
{
    private IList<Observer> observers = new List<Observer>();
    private string action;
    public void Attach(Observer o) => observers.Add(o);
    public void Detach(Observer o) => observers.Remove(o);
    public void Notify() { foreach (var o in observers) o.Update(); }
    public string SubjectState { get => action; set => action = value; }
}

class Secretary : Subject { /* 同 Boss */ }

客戶端:

Boss huhansan = new Boss();
StockObserver tongshi1 = new StockObserver("魏關巡", huhansan);
NBAObserver   tongshi2 = new NBAObserver("易管查", huhansan);

huhansan.Attach(tongshi1);
huhansan.Attach(tongshi2);

huhansan.Detach(tongshi1); // 與某同事鬧矛盾,不通知他

huhansan.SubjectState = "我胡漢三回來了!";
huhansan.Notify();

觀察者模式#

觀察者模式(Observer Pattern),又稱發布-訂閱(Publish/Subscribe)模式

定義一種一對多的依賴關係,讓多個觀察者物件同時監聽某一個主題物件。當主題物件狀態改變時,會通知所有觀察者物件,使它們自動更新自己。[DP]

結構#

  • Subject(主題/抽象通知者):把所有觀察者引用保存在一個聚合中;提供增加和刪除觀察者物件的介面
  • Observer(抽象觀察者):為所有具體觀察者定義一個更新介面(Update()
  • ConcreteSubject(具體主題):將狀態存入;當內部狀態改變時,給所有登記的觀察者發出通知
  • ConcreteObserver(具體觀察者):實作更新介面,維持本身狀態與主題狀態協調
classDiagram
    class Subject {
        <<abstract>>
        +Attach(Observer)
        +Detach(Observer)
        +Notify()
    }
    class ConcreteSubject {
        +SubjectState
    }
    class Observer {
        <<abstract>>
        +Update()*
    }
    class ConcreteObserver {
        -observerState
        +Update()
    }
    Subject o--> Observer
    Subject <|-- ConcreteSubject
    Observer <|-- ConcreteObserver
    ConcreteObserver ..> ConcreteSubject
sequenceDiagram
    participant Boss
    participant Stock as StockObserver
    participant NBA as NBAObserver
    Boss->>Boss: SubjectState = "我胡漢三回來了!"
    Boss->>Stock: Update()
    Stock-->>Stock: 關閉股票行情
    Boss->>NBA: Update()
    NBA-->>NBA: 關閉 NBA 直播
abstract class Subject
{
    private IList<Observer> observers = new List<Observer>();
    public void Attach(Observer o) => observers.Add(o);
    public void Detach(Observer o) => observers.Remove(o);
    public void Notify() { foreach (var o in observers) o.Update(); }
}

abstract class Observer
{
    public abstract void Update();
}

何時使用?#

  • 一個物件的改變需要同時改變其他物件,且不知道具體有多少對象有待改變
  • 一個抽象模型有兩個方面,其中一方依賴另一方,可以將兩者封裝在獨立的物件中,讓它們各自獨立地改變和複用

觀察者模式所做的工作其實就是解除耦合。讓耦合的雙方都依賴於抽象,而不是具體——這正是依賴倒轉原則的最佳體現。

觀察者模式的不足#

考慮 Visual Studio 點擊「執行」按鈕時,工具箱隱藏、錯誤列表隱藏、自動視窗打開、命令視窗打開……

兩個問題:

  1. 抽象通知者依賴抽象觀察者——若觀察者沒有實作介面(如 .NET 控制項,由廠商封裝),就無法成為觀察者
  2. 具體觀察者要呼叫的方法名稱可能完全不同(隱藏、打開等),不適合統一的 Update() 方法

事件委託(Event/Delegate)的解法#

.NET 提供了委託(Delegate)事件(Event) 來解決這些問題:

delegate void EventHandler();

class Boss : Subject
{
    public event EventHandler Update;
    private string action;
    public void Notify() => Update();
    public string SubjectState { get => action; set => action = value; }
}

class StockObserver
{
    public void CloseStockMarket() { ... }
}

class NBAObserver
{
    public void CloseNBADirectSeeding() { ... }
}

客戶端:

Boss huhansan = new Boss();
StockObserver tongshi1 = new StockObserver("魏關巡", huhansan);
NBAObserver   tongshi2 = new NBAObserver("易管查", huhansan);

huhansan.Update += new EventHandler(tongshi1.CloseStockMarket);
huhansan.Update += new EventHandler(tongshi2.CloseNBADirectSeeding);

huhansan.SubjectState = "我胡漢三回來了!";
huhansan.Notify();

委託是什麼#

委託(Delegate):一種引用方法的類型。一旦為委託分配了方法,委託將與該方法具有完全相同的行為

  • 委託可以看作是對函式的抽象,是函式的「類別」
  • 一個委託可以搭載多個方法,所有方法依次被喚起
  • 委託對象搭載的方法並不需要屬於同一個類別

委託的限制#

委託對象所搭載的所有方法必須具有相同的原型和形式——相同的參數列表與回傳型別。

否則,委託無法統一呼叫。

觀察者模式 vs. 委託事件#

是先有觀察者模式,再有委託事件技術。委託事件是觀察者模式的進化版本

  • 解除了通知者對「抽象觀察者介面」的依賴
  • 觀察者方法名可以不同
  • 各個觀察者甚至不需要屬於同一個類別

但兩者各有優缺點,視場景選用。