意圖
定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴它的對象都得到通知并自動更新。
結構
觀察者結構圖
觀察者時序圖
動機
將一個系統分割成一系列相互協作的類有一個常見的副作用:需要維護相關對象間的一致性。我們不希望為了維持一致性而使各類緊密耦合,因為這樣降低了它們的可重用性。
適用性
- 當一個抽象模型有兩個方面, 其中一個方面依賴于另一方面。將這二者封裝在獨立的對象中以使它們可以各自獨立地改變和復用;
- 當對一個對象的改變需要同時改變其它對象, 而不知道具體有多少對象有待改變;
- 當一個對象必須通知其它對象,而它又不能假定其它對象是誰。換言之, 你不希望這些對象是緊密耦合的。
優缺點
- 目標和觀察者間的抽象耦合(接口)。目標只知道有一系列的觀察者,但不知道它們所屬的具體類;
- 支持廣播通信。目標(Subject)發送的通知被自動廣播給所有已向該目標登記的所有對象(Observer);
- 意外的更新。如果一個觀察者(Observer)誤操作目標(Subject)的狀態,可能會導致其他觀察者連鎖反應式的錯誤更新。
注意事項
- 當觀察者(Observer)依賴多個目標(Subject)時,考慮擴展Update接口,把目標對象(Subject)作為識別參數。
// Subject class
public void Notify()
{
foreah(Observer o in Observers)
{
o.Update(this);
}
}
// ConcreteObserver class
public void Update(Subject subject)
{
if(subject is ConcreteSubject1)
{
// 來自目標1的通知
}
else if(subject is ConcreteSubject2)
{
// 來自目標2的通知
}
...
}
- 在通知機制的實現上,可以由目標(Subject)對象自動觸發,或者由客戶端手動觸發。
// 示例:狀態變更,目標(Subject)自動觸發。
public class ConcreteSubject : Subject
{
private object state;
public object SetState(object state)
{
this.state = state;
this.Notify(); // 自動觸發
}
public void Notify()
{
foreach(Observer o in Observers)
{
o.Update();
}
}
}
// 示例
public class App
{
public static void Main(string[] args)
{
ConcreteSubject subject = new ConcreteSubject();
subject.Attach(Observer); // 登記觀察者
...
// 狀態每一次變更,都會自動通知觀察者
subject.SetState(newState1);
subject.SetState(newState2);
...
}
}
// 示例:客戶端手動觸發通知
public class ConcreteSubject
{
private object state;
public object SetState(object state)
{
this.state = state;
}
public void Notify()
{
foreach(Observer o in Observers)
{
o.Update();
}
}
}
// 示例
public class App
{
public static void Main(string[] args)
{
ConcreteSubject subject = new ConcreteSubject();
subject.Attach(Observer); // 登記觀察者
...
// 設置完一系列狀態后,一次性通知觀察者(避免觀察者繁瑣更新)。
subject.SetState(newState1);
subject.SetState(newState2);
...
subject.Notify(); // 手動通知(容易遺忘)
}
}
- 確保目標(Subject)在觸發通知之前,處于一致狀態。特別是在子類集成Subject時容易發生,可用模板(Template)模式實現;
// 存在狀態不一致的錯誤代碼
public class ConcreteSubject : Subject
{
...
public override void SetState(object state)
{
base.Notify(); // 提前觸發,導致狀態不一致。
this.state = state;
}
}
更改為模板實現方式:
public class Subject
{
...
// 模板方法
public void SetState(object state)
{
this.InternalSetState(state);
this.Notify();
}
protected virtual void InternalSetState(object state)
{
this.state = state;
}
}
public class ConcreteSubject : Subject
{
...
// 子類只需要實現個性化的狀態處理
protected override void InternalSetState(object state)
{
...
this.state = state;
}
}
- 可以擴展目標的注冊接口,讓各觀察者注冊為僅對特定事件感興趣,以提高更新的效率。
public void Attach(Observer observer, InterestType interest);
示例
模擬兩個不同類型的圖形控件,分別顯示當前的時間。
實現(C#)
示例結構圖
using System;
using System.Threading;
using System.Collections.Generic;
// 目標主題基類
public class Subject
{
private readonly List<Observer> observers = new List<Observer>();
public void Attach(Observer o)
{
this.observers.Add(o);
}
public void Detach(Observer o)
{
this.observers.Remove(o);
}
public void Notify()
{
foreach(Observer o in this.observers)
{
o.Update(this);
}
}
}
// 觀察者
public abstract class Observer
{
public abstract void Update(Subject theChangedSubject);
}
// 具體的目標主題,以3秒間隔發出通知
public class ClockTimer : Subject
{
private Timer timer;
public ClockTimer()
{
this.timer = new Timer(this.Tick, null, 0, 3000);
}
public void Tick(object state)
{
this.Now = DateTime.Now;
this.Notify();
}
public DateTime Now { get; private set;}
}
// 模擬時鐘控件1
public class DigitalClock : Observer
{
private readonly ClockTimer subject;
public DigitalClock(ClockTimer subject)
{
this.subject = subject;
this.subject.Attach(this); // 注冊監聽
}
public override void Update(Subject theChangedSubject)
{
// 確認是否為目標監聽對象
if(this.subject == theChangedSubject)
{
Console.WriteLine("1.DigitalClock : " + this.subject.Now);
}
}
}
// 模擬時鐘控件2
public class AnalogClock : Observer
{
private readonly ClockTimer subject;
public AnalogClock(ClockTimer subject)
{
this.subject = subject;
this.subject.Attach(this); // 注冊監聽
}
public override void Update(Subject theChangedSubject)
{
// 確認是否為目標監聽對象
if(this.subject == theChangedSubject)
{
Console.WriteLine("2. AnalogClock : " + this.subject.Now);
}
}
}
public class App
{
public static void Main(string[] args)
{
ClockTimer timer = new ClockTimer();
DigitalClock digitalClock = new DigitalClock(timer);
AnalogClock analogClock = new AnalogClock(timer);
Console.WriteLine("please enter any key to exit..\n");
Console.Read();
}
}
// 控制臺輸出:
// please enter any key to exit..
// 1.DigitalClock : 2017/6/17 22:37:24
// 2. AnalogClock : 2017/6/17 22:37:24
// 1.DigitalClock : 2017/6/17 22:37:27
// 2. AnalogClock : 2017/6/17 22:37:27
// 1.DigitalClock : 2017/6/17 22:37:30
// 2. AnalogClock : 2017/6/17 22:37:30
// 1.DigitalClock : 2017/6/17 22:37:33
// 2. AnalogClock : 2017/6/17 22:37:33