觀察者模式(Observer)
1.1.1 摘要
在系統的設計中,我們常常需要設計一個消息提示功能,讓系統把提示信息發送到客戶端。做到這一點的設計方案可以是多種多樣,但是為了使系統能夠易于復用,我們的設計應該遵守低耦合高內聚的設計原則,而且減少對象之間的耦合有利于系統的復用。觀察者模式(Observer)是滿足這一要求的各種設計方案中最重要的一種。
觀察者模式(Observer):定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
1.1.2 正文
圖1觀察者(Observer)模式結構圖
抽象主題(Subject)角色:主題角色把所有對觀察考對象的引用保存在一個聚集里,每個主題都可以有任何數量的觀察者。抽象主題提供一個接口,可以增加和刪除觀察者對象,主題角色又叫做抽象被觀察者(Observable)角色,一般使用一個抽象類或者一個接口實現。
抽象觀察者(Observer)角色:為所有的具體觀察者定義一個接口,在得到主題的通知時更新自己。這個接口叫做更新接口。抽象觀察者角色一般用一個抽象類或者一個接口實現。在這個示意性的實現中,更新接口只包含一個方法(即Update()方法),這個方法叫做更新方法。
具體主題(ConcreteSubject)角色:將有關狀態存入具體現察者對象;在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知。具體主題角色又叫做具體被觀察者角色(Concrete Observable)。具體主題角色通常用一個具體子類實現。
具體觀察者(ConcreteObserver)角色:存儲與主題的狀態自恰的狀態。具體現察者角色實現抽象觀察者角色所要求的更新接口,以便使本身的狀態與主題的狀態相協調。如果需要,具體現察者角色可以保存一個指向具體主題對象的引用。具體觀察者角色通常用一個具體子類實現。
觀察者模式與推拉模式
推模式(Push):是一種基于客戶器/服務器機制,由服務器主動將信息發送到客戶器的技術。
拉模式(Pull)與推模式(Push)恰好相反,是由客戶器主動向服務器發送請求的技術。
接下來讓我們通過一段示例代碼介紹觀察者模式(Obsever),首先假設服務器狀態更新后要同時通知客戶端A,B,C直接的實現如下:
/// <summary> /// The Server class and when it updated, should notify the clients. /// </summary> public class Server { /// <summary> /// Keeps three objects referen. /// </summary> /// <param name="a">The object of ClentA.</param> /// <param name="b">The object of ClentB.</param> /// <param name="c">The object of ClentC.</param> public Server(ClientA a, ClientB b, ClientC c) { ClientA = a; ClientB = b; ClientC = c; } public ClientA ClientA { get; set; } public ClientB ClientB { get; set; } public ClientC ClientC { get; set; } public string SysMsg { get; set; } }
上面我們定義了服務器類,它包含了一個構造函數,通過它來保持ClientA,ClientB和ClientC類型對象的引用。
/// <summary> /// The client client, it would receive message /// when the sever updated. /// </summary> public class ClientA { /// <summary> /// Receives the message from sever. /// </summary> /// <param name="sysMsg"></param> public void Notify(string sysMsg) { SysMsg = sysMsg; } public string SysMsg { get; set; } } public class ClientB { public void Notify(string sysMsg) { SysMsg = sysMsg; } public string SysMsg { get; set; } } public class ClientC { public void Notify(string sysMsg) { SysMsg = sysMsg; } public string SysMsg { get; set; } }
接著我們定義了客戶端類ClientA,ClientB和ClientC,由于它們是被動地接受信息(推模式),所有它們只包含接受信息的方法Notify()。
圖2示例類圖
上圖我們知道Server類保持了ClientA,ClientB和ClientC類對象的引用。
static class Program { /// <summary> /// 應用程序的主入口點。 /// </summary> [STAThread] static void Main() { var server = new Server( new ClientA(), new ClientB(), new ClientC()) { SysMsg = "Hi everyone" }; server.ClientA.Notify(server.SysMsg); server.ClientB.Notify(server.SysMsg); server.ClientC.Notify(server.SysMsg); } }
這里類型Server保持了ClientA,ClientB和ClientC類型的引用,但仔細考量一下如果我們要添加新的客戶端類ClinetD,那么我們就需要修改Server的實現,這也意味著Server過度地依賴于客戶端類,這嚴重地違反了面向對象設計的原則:面向接口編程,而非面向實現編程。其實ClientA,ClinetB和ClientC三個類型都有類似的功能——Notify()方法,所以我們可以為它們抽象出一個更高層的接口,通過封裝變化使得我們設計依賴于抽象而非具體。
圖3面向接口而非實現示例類圖
現在Server類只依賴于接口IClent,如果我們需要添加新的客戶端類,只需實現IClient接口就行了,而且Server不用做出任何修改。
現在我們對于為什么要使用觀察者模式(Observer)有了初步的了解,接下來讓我們通過更詳細的例子介紹。
現在我們來實現觀察者模式(Observer),假設Gof電信公司要把他們最新發明的設計模式介紹給每一位Geek,他們想通過服務器把信息發送到每位Geek的移動客戶端,我們很榮幸協助他們完成這一項目,OK首先我們先創建抽象接口類:IObserver,然后我們創建具體觀察者:MobilePhone,Tablet和Laptop,接著創建GofTelcom作為Subject。
首先我們定義接口IObserver,它只含一個Update()方法,由于提供了統一接口使得我們設計面對接口編程,而非面向實現編程。
/// <summary> /// The interface of observer. /// </summary> public interface IObserver { void Update(string msg); }
接著定義具體的觀察者Mobile,Tablet和Laptop,它們都提供了Update()方法的具體實現。
/// <summary> /// Concrete observer. /// </summary> public class MobilePhone : IObserver { #region IObserver 成員 public void Update(string msg) { Console.WriteLine(String.Format("I am MobilePhone.\n New message: {0}", msg)); } #endregion } /// <summary> /// Concrete observer. /// </summary> public class Tablet : IObserver { #region IObserver 成員 public void Update(string msg) { Console.WriteLine(String.Format("OH...I am Tablet.\n New message: {0}", msg)); } #endregion } /// <summary> /// Concrete observer. /// </summary> public class Laptop : IObserver { #region Implementation of IObserver public void Update(string msg) { Console.WriteLine(String.Format("OH...I am Laptop.\n New message: {0}", msg)); } #endregion }
現在我們定義被觀察者類GofTelecom,它定義了一個Notify()方法,委托GofNews()和事件GofNews。如果大家想參考什么是委托和事件可以參考《.NET 中的委托》和《.NET中的委托和事件(續)》。
/// <summary> /// The Subject class. /// </summary> public class GofTelecom { public delegate void GofNews(string msg); public static event GofNews NewsEvent; /// <summary> /// Notifies every obervers. /// </summary> /// <returns>If notification is successful return true, otherwise false.</returns> public static bool Notify() { if (NewsEvent != null) { NewsEvent(Message); return false; } return true; } public static String Message { get; set; } }
最后通過給GofTelecom對象添加MobilePhone,Tablet和Laptop對象來實現觀察者和被觀察者之間的關聯。
static void Main(string[] args) { IList<IObserver> objObserver = new List<IObserver>(); // Registers observer. objObserver.Add(new MobilePhone()); objObserver.Add(new Tablet()); objObserver.Add(new Laptop()); // Attachs event and method. foreach (IObserver ob in objObserver) { GofTelecom.NewsEvent += ob.Update; } GofTelecom.Message = "Hi Everyone,/n We recommand you new Design Pattern."; Console.WriteLine(!GofTelecom.Notify() ? "Notify successful.\n" : "Notify failed.\n"); Console.ReadKey(); }
圖4示例輸出結果
在系統設計過程中,我們的系統設計基本上都要提供消息提示功能類似于QQ或MSN的消息提示框,而且這個功能恰好是觀察者模式(Observer)實現的一種具體實現,在我們實現觀察者模式(Observer)消息提示功能之前,先讓我們回顧一下推拉模式吧!
推模式和拉模式的區別:
也許大家會好奇地問“使用推模式和拉模式,有什么區別呢?”
推模式的好處是能夠及時響應,想要提供給Observer端什么數據,就將這些數據封裝成對象,傳遞給Observer,缺點是需要創建自定義的事件關聯信息,而且它必須繼承于EventArgs對象。
缺點:精確性較差,不能保證能把信息送到客戶器。
拉模式的好處則是不需要另外定義對象,直接將自身的引用傳遞進去就可以了。
缺點:不能夠及時獲取系統的變更。
.Net 中沒有內置的IObserver和IObservable接口(Java中提供該接口類),我們可以通過委托和事件來完成,但是一樣面臨選擇推模式還是拉模式的問題,何時使用哪種策略完全依賴于設計者,你也可以將兩種方式都實現了,接下來我們的消息提示功能將通過拉模式實現。
首先我們通過給窗體控件添加屬性TimerStatus,客戶端通過Timer來發送消息請求到服務器端,TimerStatus是通過控件公開Timer屬性的設置,具體實現如下:
/// <summary> /// The timer status, /// it includes base properties to set up timer. /// </summary> [TypeConverterAttribute(typeof(ClockStatusTypeConverter))] public class TimerStatus { private Timer _timer = new Timer(); private Frequencies _frequencies = Frequencies.Second; private int _interval = 23; private bool _start; /// <summary> /// Paramenterless constructor. /// </summary> public TimerStatus() : this(Frequencies.Minute, 23, true) { } public TimerStatus(Frequencies frequencies, int interval, bool start) { Frequencies = frequencies; Interval = interval; Start = start; } /// <summary> /// Gets or sets the frequencies. /// </summary> /// <value> /// The frequencies. /// </value> public Frequencies Frequencies { get { return _frequencies; } set { _frequencies = value; } } /// <summary> /// Gets or sets the interval. /// </summary> /// <value> /// The interval. /// </value> public int Interval { get { return _interval; } set { _interval = value; } } /// <summary> /// Gets or sets a value indicating /// whether this <see cref="TimerStatus"/> is start. /// </summary> /// <value> /// <c>true</c> if start; otherwise, <c>false</c>. /// </value> public bool Start { get { return _start; } set { _start = value; } } public Timer Timer { get { if (null == _timer) new Timer(); return _timer; } set { _timer = value; } } }
定義一個頻率枚舉,它提供Second,Minute和Hour單位來設置Timer的頻率。
/// <summary> /// The frequency of timer. /// </summary> public enum Frequencies { Second, Minute, Hour }
現在我們完成了控件屬性TimerStatus的定義,接著把該屬性添加到我們自定義Form控件中,我們只需在自定義Form中添加屬性如下:
/// <summary> /// Gets or sets the timer status. /// </summary> /// <value> /// The timer status. /// </value> [Browsable(true)] public TimerStatus TimerStatus { get { if (null == _timerStatus) return new TimerStatus(); return _timerStatus; } set { _timerStatus = value; } }
但我們發現自定義控件屬性并不能編輯,這是由于控件沒有辦法把我們自定義類型TimeStatus轉換為控件中實現的類型(如:字符串)。所以我們需要添加轉換器類。
圖5消息提示控件屬性定義
/// <summary> /// Defines <see cref="TimerStatus"/> type converter. /// </summary> public class TimerStatusTypeConverter : ExpandableObjectConverter { /// <summary> /// Checks whether can convert from a string to <see cref="TimerStatus"/> or not. /// </summary> /// <param name="context"></param> /// <param name="sourceType">Source Type.</param> /// <returns>Can return true, otherwise false.</returns> public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { // Can convert from String to ClockStatus. if (sourceType == typeof(String)) return true; return base.CanConvertFrom(context, sourceType); } /// <summary> /// Checks whether can convert from <see cref="TimerStatus"/> to string or not. /// </summary> /// <param name="context"></param> /// <param name="destinationType">Destination Type</param> /// <returns>Can return true, otherwise false.</returns> public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(String)) return true; return base.CanConvertTo(context, destinationType); } /// <summary> /// Converts from stirng to <see cref="TimerStatus"/>. /// </summary> public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { var convertValue = value as string; if (String.IsNullOrEmpty(convertValue)) return new TimerStatus(); // Defines a enum convert. var enumConvert = TypeDescriptor.GetConverter(typeof(Frequencies)); // Get the string corresponding to control properties. var values = convertValue.Split(','); if (values.Length < 3) throw new ArgumentException("Wrong number of arguments"); int result1 = -1; bool result2; if (int.TryParse(values[1].ToString(), out result1) && bool.TryParse(values[2].ToString(), out result2)) return new TimerStatus( (Frequencies)enumConvert.ConvertFromString(values[0]), result1, result2); return base.ConvertFrom(context, culture, value); } /// <summary> /// Converts from <see cref="TimerStatus"/> stirng. /// </summary> public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { if (value is TimerStatus) { if (destinationType == typeof(String)) { var clockStatus = value as TimerStatus; return String.Format("{0},{1},{2}", clockStatus.Frequencies, clockStatus.Interval, clockStatus.Start); } } return base.ConvertTo(context, culture, value, destinationType); } }
上面我們定義了自定義類型轉換器,它提供把TimerStatus類型轉換為字符串的方法和通過屬性參數創建TimerStatus對象的方法。
圖6消息提示控件屬性定義
現在我們定義好了消息提示框,而且我們可以通過屬性瀏覽器直接編輯TimerStatus,如Frequencies設置Timer的頻率,Interval時間間隔和Start是否啟用Timer。
每個客戶端通過拉模式訪問服務器端,當有信息就返回到服務器端顯示出來如下:
圖7消息提示框設計
1.1.3 總結
觀察者模式(Observer)的優點是實現了抽象與具體的分離,并定義了穩定的更新消息傳遞機制,類別清晰,并抽象了更新接口。
但是其缺點是每個觀察者對象必須繼承這個抽像出來的接口類,這樣就造成了一些不方便,比如有一個別人寫的觀察者對象,并沒有繼承該抽象類,或者接口不對,我們又希望不修改該類直接使用它。雖然可以再應用Adapter模式來一定程度上解決這個問題,但是會造成更加復雜煩瑣的設計,增加出錯幾率。
觀察者模式(Observer)的效果有以下幾個優點:
- 觀察者模式在被觀察者和觀察者之間建立一個抽象的耦合。被觀察者角色所知道的只是一個具體現察者聚集,每一個具體現察者都符合一個抽象觀察者的接口。被觀察者并不認識任何一個具體觀察者,它只知道它們都有一個共同的接口。由于被觀察者和觀察者沒有緊密地耦合在一起,因此它們可以屬于不同的抽象化層次。
- 觀察者模式支持廣播通信。被觀察者會向所有的登記過的觀察者發出通知。
觀察者模式(Observer)有下面的一些缺點:
- 如果一個被觀察者對象有很多直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間。
- 如果在被觀察者之間有循環依賴的話,被觀察者會觸發它們之間進行循環調用,導致系統崩潰。在使用觀察考模式時要特別注意這一點。
- 如果對觀察者的通知是通過另外的線程進行異步投遞的話,系統必須保證投遞是以自恰的方式進行的。
- 雖然觀察者模式可以隨時使觀察者知道所觀察的對象發生了變化,但是觀察者模式沒有相應的機制使觀察者知道所觀察的對象是怎么發生變化的
參考
http://www.codeproject.com/KB/miscctrl/RobMisNotifyWindow.aspx
|
|
關于作者:[作者]:
JK_Rush從事.NET開發和熱衷于開源高性能系統設計,通過博文交流和分享經驗,歡迎轉載,請保留原文地址,謝謝。 |

1.1.1 摘要 在系統的設計中,我們常常需要設計一個消息提示功能,讓系統把提示信息發送到客戶端。做到這一點的設計方案可以是多種多樣,但是為了使系統能夠易于復用,我們的設計應該遵守低...







浙公網安備 33010602011771號