問題:
在軟件系統開發中經常需要為某些對象建立一些依賴關系,而這些依賴於該對象的依賴者會根據該對象的狀態變化,觸發某些事件或方法也做出相應的改變,我們怎么樣建立這種依賴關系,並做到當對象狀態發生變化時對依賴對象的通知?
定義:
觀察者模式是對象的行為模式,又叫發布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。
觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
意圖:
提供一個目標(Subject)對象,他提供依賴於它的觀察者Observer的注冊(Attach:將Observer注冊到Subject中,Subject將Observer一個Container中。)和注銷(Detach:Observer告訴Subject要撤銷觀察,被觀察者從Container中將觀察者去除。)操作,並且提供了使得依賴於它的所有觀察者的通知操作(Notify),當Subject目標狀態發生改變后Subject對象發出Notify通知所有Observer進行修改(調用Update)。
參與者:
•抽象主題(Subject)角色:
抽象主題提供一個接口,可以增加和刪除觀察者對象,抽象主題角色又叫做抽象被觀察者(Observable)角色。
•具體主題(ConcreteSubject)角色:
維護對所有具體觀察者的引用的列表,將有關狀態存入具體觀察者對象;在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知。具體主題角色又叫做具體被觀察者(Concrete Observable)角色。
•抽象觀察者(Observer)角色:
為所有的具體觀察者定義一個接口;定義了一個update方法,在得到主題的通知時被調用,這個接口叫做更新接口。
•具體觀察者(ConcreteObserver)角色:
實現抽象觀察者角色所要求的更新接口,通過update()方法接收ConcreteSubject的通知,並作出具體的處理。如果需要,具體觀察者角色可以保持一個指向ConcreteSubject對象的引用(用於ConcreteSubject狀態信息的傳遞)。
UML:
實例說明:
諾基亞手機工廠
工廠在接到生產任務后,通知工廠的各個零部件生產組,先生產手機需要的零部件
uml圖如下:
代碼:
/// 主題對象接口
/// </summary>
public interface INokiaSubject
{
void Attach(IPhoneComponentObserver phoneComponentObserver);
void Detach(IPhoneComponentObserver phoneComponentObserver);
void NodifyCreateComponent();
}
/// <summary>
/// 觀察者接口
/// </summary>
public interface IPhoneComponentObserver
{
/// <summary>
/// 觀察者方法,接收通知生產手機部件
/// </summary>
void CreateComponent( string type, int count);
}
/// <summary>
/// 手機cpu 具體觀察者接口
/// </summary>
public class CpuObserver : IPhoneComponentObserver
{
public void CreateComponent( string type, int count)
{
Console.WriteLine( " 生產了{0}個{1}手機的CPU. ", count, type);
}
}
/// <summary>
/// 手機主板 具體觀察者接口
/// </summary>
public class MbObserver : IPhoneComponentObserver
{
public void CreateComponent( string type, int count)
{
Console.WriteLine( " 生產了{0}個{1}手機的主板. ", count, type);
}
}
/// <summary>
/// 手機生產工廠, 具體主題對象類
/// </summary>
public class PhoneFactoryObserver : INokiaSubject
{
string type;
int count;
List<IPhoneComponentObserver> container = new List<IPhoneComponentObserver>();
public PhoneFactoryObserver( string _type, int _count)
{
this.type = _type;
this.count = _count;
}
/// <summary>
/// 注冊
/// </summary>
/// <param name="phoneComponentObserver"></param>
public void Attach(IPhoneComponentObserver phoneComponentObserver)
{
container.Add(phoneComponentObserver);
}
/// <summary>
/// 移除
/// </summary>
/// <param name="phoneComponentObserver"></param>
public void Detach(IPhoneComponentObserver phoneComponentObserver)
{
container.Remove(phoneComponentObserver);
}
/// <summary>
/// 通知
/// </summary>
public void NodifyCreateComponent()
{
foreach (IPhoneComponentObserver obj in container)
{
obj.CreateComponent(type, count);
}
}
public void ProductionPhone()
{
System.Console.WriteLine( " 生產了{0}台,{1}手機 ", this.count, this.type);
}
}
/// <summary>
/// 客戶端測試
/// </summary>
public void ObserverTest()
{
// 工廠准備生產 20 部 N8
PhoneFactoryObserver ns = new PhoneFactoryObserver( " N8 ", 20);
// 注冊生產部cpu生產組
ns.Attach( new CpuObserver());
// 注冊生產部主板生產組
ns.Attach( new MbObserver());
// 通知注冊的 手機 部件生產組
ns.NodifyCreateComponent();
// 生產手機
ns.ProductionPhone();
}
.NET中的Observer模式:
委托可以充當抽象的Observer接口,而提供事件的對象充當了目標對象,使用委托,只需向Subject注冊,通知時,調用的Observer的update()即可,不需要注冊Observer到Subject中,因此委托是比抽象Observer接口更耦合更低,更為靈活。
/// 定義一個委托,用於注冊需要通知的方法
/// </summary>
/// <param name="type"></param>
/// <param name="count"></param>
public delegate void NotifyComponentObserverEventHandler( string type, int count);
/// <summary>
/// 主題對象接口
/// </summary>
public interface INokiaSubject
{
NotifyComponentObserverEventHandler NotifyComponentObserverEvent { get; set; }
void NodifyCreateComponent();
}
/// <summary>
/// 手機cpu 具體觀察者接口
/// </summary>
public class CpuObserver
{
public void CreateCpuComponent( string type, int count)
{
Console.WriteLine( " 生產了{0}個{1}手機的CPU. ", count, type);
}
}
/// <summary>
/// 手機主板 具體觀察者接口
/// </summary>
public class MbObserver
{
public void CreateComponent( string type, int count)
{
Console.WriteLine( " 生產了{0}個{1}手機的主板. ", count, type);
}
}
/// <summary>
/// 手機生產工廠, 具體主題對象類
/// </summary>
public class PhoneFactoryObserver : INokiaSubject
{
string type;
int count;
public NotifyComponentObserverEventHandler NotifyComponentObserverEvent { get; set; }
public PhoneFactoryObserver( string _type, int _count)
{
this.type = _type;
this.count = _count;
}
/// <summary>
/// 通知方法~
/// </summary>
public void NodifyCreateComponent()
{
if (NotifyComponentObserverEvent != null)
{
NotifyComponentObserverEvent(type, count);
}
}
public void ProductionPhone()
{
System.Console.WriteLine( " 生產了{0}台,{1}手機 ", this.count, this.type);
}
}
/// <summary>
/// 客戶端測試
/// </summary>
public void ObserverTest()
{
// 工廠准備生產 20 部 N8
PhoneFactoryObserver ns = new PhoneFactoryObserver( " N8 ", 20);
// 注冊生產部cpu生產組
CpuObserver cpuObserver = new CpuObserver();
ns.NotifyComponentObserverEvent += new NotifyComponentObserverEventHandler(cpuObserver.CreateCpuComponent);
// 注冊生產部主板生產組
MbObserver mbObserver = new MbObserver();
ns.NotifyComponentObserverEvent += new NotifyComponentObserverEventHandler(mbObserver.CreateComponent);
// 通知 手機 部件生產組
ns.NodifyCreateComponent();
// 生產手機
ns.ProductionPhone();
}
優點:
•Subject和Observer之間是松偶合的,所以觀察者模式把一對多對象之間的通知依賴關系的變得更為松散,大大提高了程序的可維護性和可擴展性,也很好的符合了開放-封閉原則
•Subject在發送廣播通知的時候,無需太多的知道Observer的細節,只需調用更新接口即可。
缺點:
•如果一個Subject有大量Observer訂閱的,廣播通知是以順序循環遍歷的方式通知的,會存在一定效率問題。如果前一個Observer通訊產生異常,后面的通知就需要等待或不被執行
•所有對於一個具體的Subject,所有的Observer,得到的通知都是相同的,不能根據需要為其特殊定制。
應用情景:
•對一個對象狀態的更新,需要其他對象同步更新,而且其他對象的數量動態可變。
•對象僅需要將自己的更新通知給其他對象而不需要知道其他對象的細節。
觀察者模式兩種方式:
•推模型:
主題對象向觀察者推送主題的詳細信息(參數),不管觀察者是否需要,推送的信息通常是主題對象的全部或部分數據。
•拉模型:
主題對象在通知觀察者的時候,傳遞一個主題對象自身的引用,觀察者根據需要從主題對象引用中“拉取”需要的數據。
•區別:
推模型是假定主題對象知道觀察者需要的數據;而拉模型是主題對象不知道觀察者具體需要什么數據,觀察者自己去按需要取值。
推模型可能會使得觀察者對象難以復用,因為觀察者的update()方法是按需要定義的參數,可能無法兼顧沒有考慮到的使用情況。這就意味着出現新情況的時候,就可能提供新的update()方法,或者重構觀察者;而拉模型就不會造成這樣的情況,因為拉模型下,update()方法的參數是主題對象本身,這基本上是主題對象能傳遞的最大數據集合了,基本上可以適應各種情況的需要。