引子:老闆回來,我不知道#
小菜公司同事們上班偷看股票行情,老闆出門時請前台秘書童子喆當「眼線」——一旦老闆回辦公室就打電話通知,大家好各就各位。
但這天老闆回來時順手把童子喆叫去印文件,沒人來得及打電話。背對門的魏關巡還大喊「我的股票漲停了哦」,回頭就看到老闆憤怒的臉。
一個訊息要通知多個對象——這就是觀察者模式的典型場景。
第一版:雙向耦合#
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} 關閉股票行情,繼續工作!");
}
}
Secretary與StockObserver互相耦合:
- 若新增「看 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 ..> ConcreteSubjectsequenceDiagram
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 點擊「執行」按鈕時,工具箱隱藏、錯誤列表隱藏、自動視窗打開、命令視窗打開……
兩個問題:
- 抽象通知者依賴抽象觀察者——若觀察者沒有實作介面(如 .NET 控制項,由廠商封裝),就無法成為觀察者
- 具體觀察者要呼叫的方法名稱可能完全不同(隱藏、打開等),不適合統一的
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. 委託事件#
是先有觀察者模式,再有委託事件技術。委託事件是觀察者模式的進化版本:
- 解除了通知者對「抽象觀察者介面」的依賴
- 觀察者方法名可以不同
- 各個觀察者甚至不需要屬於同一個類別
但兩者各有優缺點,視場景選用。