觀察者(Observer)

意圖

定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴它的對象都得到通知并自動更新。

結構

觀察者結構圖
觀察者時序圖

動機

將一個系統分割成一系列相互協作的類有一個常見的副作用:需要維護相關對象間的一致性。我們不希望為了維持一致性而使各類緊密耦合,因為這樣降低了它們的可重用性。

適用性

  • 當一個抽象模型有兩個方面, 其中一個方面依賴于另一方面。將這二者封裝在獨立的對象中以使它們可以各自獨立地改變和復用;
  • 當對一個對象的改變需要同時改變其它對象, 而不知道具體有多少對象有待改變;
  • 當一個對象必須通知其它對象,而它又不能假定其它對象是誰。換言之, 你不希望這些對象是緊密耦合的。

優缺點

  • 目標和觀察者間的抽象耦合(接口)。目標只知道有一系列的觀察者,但不知道它們所屬的具體類;
  • 支持廣播通信。目標(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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容