重温Observer模式--热水器·改(转载)
引言
在 C#中的委托和事件 一文的后半部分,我向大家講述了Observer(觀察者)模式,并使用委托和事件實現了這個模式。實際上,不使用委托和事件,一樣可以實現Observer模式。在本文中,我將使用GOF的經典方式,再次實現一遍Observer模式,同時將講述在 C#中的委托和事件 一文中沒有提及的推模式(Push)和拉模式(Pull)。
設計思想概述
在 C#中的委托和事件 一文后半部分中我已經較詳細的講述了Observer設計模式的思想,所以這里僅簡單的提及一下。Observer設計模式中實際上只包含了兩類對象,一個是Subject(主題),一個是Observer(觀察者)。它們之間的角色是:
- Subject:主題(被監視對象),它往往包含著Observer所感興趣的內容。
- Observer:觀察者,它觀察Subject。當Subject中的某件事發生的時候(通常是它所感興趣的內容改變的時候),會被自動告知,而Observer則會采取相應的行動(通常為更新自身狀態或者顯示輸出)。
它們之間交互的核心工作流程就是:
- Register()方法實現為:它接收一個Observer的引用作為參數,并保存此引用。
- 保存的方式通常為在 Subject內聲明一個集合類,比如:List<Observer>。
- 一個Subject可以供多個Observer注冊。
- Notify的方法實現為:遍歷保存Observer引用的集合類,然后在Observer的引用上調用Update方法,更新Observer。
- 某件事是一個不確定的事,對于熱水器來說,這個事就是“溫度達到一定高度”。它對外界暴露的方法,應該是“燒水” -- BoilWater(),而不是Notify(),所以Notify通常實現為私有方法。
Observer 向 Subject 注冊的序列圖表示如下:
Subject事件觸發時,通知Observer調用Update()方法的序列圖如下:
模式的接口定義
按照面向對象設計的原則:面向接口編程,而非面向實現編程。那么現在應該首先定義Subject和Observer的接口,我們可能很自然地會想到將這兩個接口分別命名為 ISubjcet 和 IObserver。而實際上,據我查閱的一些資料,這里約定俗成的命名為:IObservable 和 IObserver,其中由 Subject 實現 IObservable。
NOTE:可能很多人和我當初一樣困惑,命名為ISubject不是很好么,為什么叫 IObservable?我參考了一些資料,大概的解釋是這樣的:接口定義的是一個行為,表示的是一種能力,所以對于接口的命名最好用動詞的形容詞或者名詞變體。這里,Observe是一個動詞,意為觀察,Observer是動詞的名詞變體,意為觀察者;Observable是動詞的形容詞變體,表示為可觀察的。類似的例子有很多,比如IComparable 和 IComparer 接口、IEnumerable 和 IEnumerator 接口等。
現在我們先來看Subject需要實現的接口IObservable。
IObservable接口
首先創建解決方案ObserverPattern,并在其下添加控制臺項目ConsoleApp,然后假如IObservable.cs文件,來完成這個接口。如同我們上面分析的,Suject將實現這個接口,它只用定義兩個方法 Register()和Unregister:
public interface IObservable {
??? void Register(IObserver obj);?????? // 注冊IObserver
??? void Unregister(IObserver obj);???? // 取消IObserver的注冊
}
注意它的兩個方法接收 IObserver類型的對象,分別用于注冊和取消注冊。
IObserver 接口
現在我們再來完成IObserver接口,所有的Observer都需要實現這個接口,以便在事件發生時能夠被 自動告知(自動調用其Update()方法,改變自身狀態),它僅包含一個Update()方法:
public interface IObserver {
??? void Update();????? // 事件觸發時由Subject調用,更新自身狀態
}
再強調一遍,這里的關鍵就是Update()方法不是由Observer本身調用,而是由Subject在某事發生時調用。
抽象基類 SubjectBase
注意到上面序列圖中的Container(容器),它用于保存IObserver引用的方式,對于很多IObservable的實現來說可能都是一樣的,比如說都用List<IObserver>或者是Hashtable等。所以我們最好再定義一個抽象類,讓它實現 IObservable 接口,并使用List<IObserver>作為容器的一個默認實現,以后我們再創建實現IObservalbe的類(Subject),只需要繼承這個基類就可以了,這樣可以更好地代碼重用:
public abstract class SubjectBase : IObservable {
??? // 使用一個 List<T> 作為 IObserver 引用的容器
??? private List<IObserver> container = new List<IObserver>();
???
??? public void Register(IObserver obj) {
?????? container.Add(obj);
??? }
??? public void Unregister(IObserver obj) {
?????? container.Remove(obj);
??? }
??? protected virtual void Notify() {?????? // 通知所有注冊了的Observer
?????? foreach (IObserver observer in container) {
?????????? observer.Update();?????????? // 調用Observer的Update()方法
?????? }
??? }
}
有了這樣兩個接口,一個抽象類我們的UML類圖便可以畫出來了:
注意這里也可以不使用IObservable接口,直接定義一個抽象類,定義IObservable接口能進一步的抽象,更靈活一些,可以基于這個接口定義出不同的抽象類來(主要區別為Container的實現不同,可以用其他的集合類)。
Observer模式的實現
現在我們來實現Observer模式,我們先創建我們的實體類(Concrete Class):熱水器(Heater),報警器(Alarm),顯示器(Screen)。其中,熱水器是Subject,報警器和顯示器是Observer。報警器和顯示器關心的東西是熱水器的水溫,當熱水器的水溫大于97度時,顯示器需要顯示“水快燒開了”,報警器發出聲音,也提示“嘟嘟嘟,水快燒開了”。
下面的代碼非常的簡單明了,也添加了注釋,我就不做說明了:
熱水器(Subject)的實現
熱水器繼承自SujectBase基類,并添加了BoilWater()方法。
public class Heater : SubjectBase {
??? private string type;????????????? // 添加型號作為演示
??? private string area;????????????? // 添加產地作為演示
??? private int temprature;???????? // 水溫
??? public Heater(string type, string area) {
?????? this.type = type;
?????? this.area = area;
?????? temprature = 0;
??? }
??? public string Type { get { return type; } }
??? public string Area { get { return Area; } }
??? public Heater() : this("RealFire 001", "China Xi'an") { }
??? // 供子類覆蓋,以便子類拒絕被通知,或添加額外行為
??? protected virtual void OnBoiled() {
?????? base.Notify(); // 調用父類Notify()方法,進而調用所有注冊了的Observer的Update()方法
??? }
??? public void BoilWater() {?????? // 燒水
?????? for (int i = 0; i <= 99; i++) {
?????????? temprature = i+1;
?????????? if (temprature > 97) {?????? // 當水快燒開時(溫度>97度),通知Observer
????????????? OnBoiled();
?????????? }
?????? }
??? }
}
報警器 和 顯示器 (Observer)的實現
報警器(Alarm)和顯示器(Screen)的實現是類似的,僅僅為了說明多個Observer可以注冊同一個Subject。
// 顯示器
public class Screen : IObserver {
??? // Subject在事件發生時調用,通知Observer更新狀態(通過Notify()方法)
??? public void Update() {
?????? Console.WriteLine("Screen".PadRight(7) + ": 水快燒開了。");
??? }
}
// 報警器
public class Alarm : IObserver {
??? public void Update() {
?????? Console.WriteLine("Alarm".PadRight(7) + ":嘟嘟嘟,水溫快燒開了。");
??? }
}
運行程序
接下來,我們運行一下程序:
class Program {
??? static void Main(string[] args) {
?????? Heater heater = new Heater();
?????? Screen screen = new Screen();
?????? Alarm alarm = new Alarm();
?????? heater.Register(screen);???? // 注冊顯示器
?????? heater.Register(alarm);???????? // 注冊熱水器
?????? heater.BoilWater();???????????? // 燒水
?????? heater.Unregister(alarm);??? // 取消報警器的注冊
?????? Console.WriteLine();
?????? heater.BoilWater();???????????? // 再次燒水
??? }
}
輸出為:
Screen : 水快燒開了。
Alarm? :嘟嘟嘟,水快燒開了。
Screen : 水快燒開了。
Alarm? :嘟嘟嘟,水快燒開了。
Screen : 水快燒開了。
Alarm? :嘟嘟嘟,水快燒開了。
Screen : 水快燒開了。
Screen : 水快燒開了。
Screen : 水快燒開了。
推模式 和 拉模式
像上面這種實現方式,基本上是沒有太大意義的。比如說,我們通常會希望在Screen上能夠即時地顯示水的溫度,而且當水在100度的時候顯示“水已經燒開了”,而非“水快燒開了”。我們還可能希望顯示熱水器的型號和產地。所以我們需要 在Observer的Update()方法中能夠獲得 Subject中所發生的事件的進展狀況 或者事件觸發者Suject的狀態和屬性。在本例中事件的進展狀況,就是水的溫度;事件觸發者(Suject)的狀態和屬性,則為 熱水器的型號和產地。此時,我們有兩種策略,一種是 推模式,一種是拉模式,我們先看看推模式。
Observer中的推模式
顧名思義,推模式就是Subject在事件發生后,調用Notify時,將事件的狀況(水溫),以及自身的屬性(狀態)封裝成一個對象,推給Observer。而如何推呢?當然是通過Notify()方法,讓Notify()方法接收這個對象,在Notify()方法內部,再次將對象傳遞給Update()方法了。那么現在要做兩件事:1、創建新類型,這個類型封裝了我們想要推給Observer(顯示器)的事件進展狀況(水溫),以及事件觸發者Subject(熱水器)的屬性(或者叫狀態)。
我們在ObserverPattern解決方案下重新建一個控制臺項目,起名為ConsoleApp2,并設置為啟動項目。將上一項目ConsoleApp中的文件復制進來,然后我們創建一個新類型BoiledEventArgs,用它來封裝我們推給Observer的數據。
public class BoiledEventArgs {
??? private int temperature;???? // 溫度
??? private string type;???????? // 類型
??? private string area;???????? // 產地
??? public BoiledEventArgs(int temperature, string type, string area) {
?????? this.temperature = temperature;
?????? this.type = type;
?????? this.area = area;
??? }
??? public int Temperature { get { return temperature; } }
??? public string Type { get { return type; } }
??? public string Area { get { return area; } }
}
注意這個類型的命名雖然為BoiledEventArgs,但是和.Net中的內置類型EventArgs沒有任何聯系,只是起了這樣一個名字。
2、我們需要依次修改 IObserver接口,Screen類的Update()方法,SubjectBase類,以及Heater類,讓他們可以接收這個EventArgs參數。出于示范的目的,后面的例子我都將不再使用警報器Alarm類,它的存在僅僅是為了說明多個Observer可以注冊一個Subject,上面我們已經示范過了,所以現在我們把它刪掉。
我們先來看下IObserver接口:
public interface IObserver {
??? // 推模式的實現方式,接收一個BoiledEventArgs
??? void Update(BoiledEventArgs e);
}
接口變了,顯示器(Screen)的實現也需要修改:
public class Screen : IObserver {
??? private bool isDisplayedType = false;????? // 標記變量,標示是否已經打印過
??? public void Update(BoiledEventArgs e) {
?????? // 打印產地和型號,只打印一次
?????? if (!isDisplayedType) {
?????????? Console.WriteLine("{0} - {1}: ", e.Area, e.Type);
?????????? Console.WriteLine();
?????????? isDisplayedType = true;
?????? }
?????? if (e.Temperature < 100) {??
?????????? Console.WriteLine(
????????????? String.Format("Alarm".PadRight(7) + ":水快燒開了,當前溫度:{0}。", e.Temperature));
?????? } else {
?????????? Console.WriteLine(
????????????? String.Format("Alarm".PadRight(7) + ":水已經燒開了!!"));?? ?????????????
?????? }
??? }
}
現在可以看到,在Update()方法中,通過傳遞進來的BoiledEventArgs參數,我們可以獲得事件進展(溫度),以及事件觸發者的信息(產地和型號)了。
接下來我們看這個 BoiledEventArgs是如何傳遞給 Update()方法的,我們看下SubjectBase基類 和 熱水器Heater需要做怎樣的修改:
public abstract class SubjectBase : IObservable {
??? // 其余略...
??? protected virtual void Notify(BoiledEventArgs e) {??? // 通知所有注冊了的Observer
?????? foreach (IObserver observer in container) {
?????????? observer.Update(e);????????? // 調用Observer的Update()方法
?????? }
??? }
}
public class Heater : SubjectBase {
??? // 其余略 ...
??? // 供子類覆蓋,以便子類拒絕被通知,或者添加額外行為
??? protected virtual void OnBoiled(BoiledEventArgs e) {
?????? base.Notify(e);????????? // 調用基類方法,通知Observer
??? }
??? public void BoilWater() {?????? // 燒水
?????? for (int i = 0; i <= 99; i++) {
?????????? temprature = i + 1;
?????????? if (temperature > 97) {????? // 當水快燒開時(溫度>97度),通知Observer
???????????? BoiledEventArgs e = new BoiledEventArgs(temperature, type, area);
????????????? OnBoiled(e);
?????????? }
?????? }
??? }
}
我們看到,在事件發生時(水溫>97度),我們根據事件進展狀況和熱水器的屬性創建了BoiledEventArgs類型的實例,并且傳遞給了OnBoiled()方法,進而調用了基類的方法,傳遞了該實例。
我們再次對程序進行一下測試:
class Program {
??? static void Main(string[] args) {
?????? Heater heater = new Heater();
?????? Screen screen = new Screen();
?????? heater.Register(screen);???? // 注冊顯示器
?????? heater.BoilWater();???????????? // 燒水
??? }
}
輸出為:
ChinaXi'an - RealFire 001:
Alarm? :水快燒開了,當前溫度:98。
Alarm? :水快燒開了,當前溫度:99。
Alarm? :水已經燒開了!!
Observer 中的拉模式
繼續進行之前,我們在ObserverPattern解決方案下,再創建一個新的Console項目,命名為ConsoleApp3,然后把ConsoleApp2 項目下的文件拷貝過來,把啟動項目設置為ConsoleApp3。
拉模式的意思就是說,Subject(熱水器)在事件發生時(水溫超過97度),并非將自身狀態封裝成對象通過Notify()方法,進而再通過Observer的引用,調用Update()方法傳遞給Observer(顯示器),而是直接將自身的引用(以基類或者Object的形式)傳遞過去。Observer在Update()方法中,對傳遞進來的引用進行一個向下轉換(Downcast),轉換成具體的Subject類(比如熱水器),然后通過這個引用調用Subject實體類(熱水器)的公共屬性獲取狀態信息(從中把有用數據拉出來 :-)。
我們需要再次對IObserver接口的Update()方法修改,相應的修改還要修改SubjectBase基類、Heater類 以及 IObserver接口的實現--顯示器類(Screen)。
public interface IObserver {
??? // 拉模式的Update()方法定義
??? void Update(IObservable sender);
}
注意這里接收一個IObservable類型作為Update()方法的參數,而IObservable接口本身只包含Regesiter()和Unregister()兩個方法,所以在IObserver的實現中,這里要進行向下轉換,轉換為響應的實體類對象,才能獲得對象的屬性。這里也可以接受一個Object類型參數。
我們現在看這個接口的實現,顯示器類(Screen):
public class Screen : IObserver {
??? private bool isDisplayedType = false;
??? public void Update(IObservable obj) {
?????? // 這里存在一個向下轉換(由繼承體系中高級別的類向低級別的類轉換)。
?????? Heater heater = (Heater)obj;
?????? // 打印產地和型號,只打印一次
?????? if (!isDisplayedType) {
?????????? Console.WriteLine("{0} - {1}: ", heater.Area, heater.Type);
?????????? Console.WriteLine();
?????????? isDisplayedType = true;????????????
?????? }
?????? if (heater.Temperature < 100) {???? // 通過熱水器引用heater獲取溫度
?????????? Console.WriteLine(
????????????? String.Format("Alarm".PadRight(7) + ":水快燒開了,當前溫度:{0}。", heater.Temperature));
?????? } else {
?????????? Console.WriteLine(
????????????? String.Format("Alarm".PadRight(7) + ":水已經燒開了!!"));?? ?????????????
?????? }
??? }
}
接下來我們再看下 SubjectBase基類,以及熱水器Heater的修改:
public class SubjectBase
??? // 其余略...
??? // 接受一個 IObservable 類型
??? protected virtual void Notify(IObservable obj) {????? // 通知所有注冊了的Observer
?????? foreach (IObserver observer in container) {
?????????? observer.Update(obj);??????? // 調用Observer的Update()方法
?????? }
??? }
}
public class Heater : SubjectBase {
??? // 其余略...
??? // 新添屬性 Temperature
??? public int Temperature { get { return temperature; } }
??? // 供子類覆蓋,以便子類拒絕被通知,或者添加額外行為
??? protected virtual void OnBoiled() {
?????? base.Notify(this);?????????? // <-- 將本身傳遞過去
??? }
??? public void BoilWater() {?????? // 燒水
?????? for (int i = 0; i <= 99; i++) {
?????????? temperature = i+1;
?????????? if (temperature > 97) {???????? // 當水快燒開時(溫度>97度),通知Observer
????????????? OnBoiled();?????????? // <-- 修改了這里
?????????? }
?????? }
??? }
}
注意,Heater類以前不提供對temperature字段的訪問,而為了能在Observer(顯示器)的Update()方法中的通過引用訪問到temperature,我們需要為Heater類再添加一個 Temperature屬性:
public int Temperature { get { return temperature; } }
而在調用Notify()方法時,我們通過this關鍵字將對熱水器Heater本身的引用傳遞了進去:
base.Notify(this);?????????? // <-- 將本身傳遞過去
我們再來做個測試:
class Program {
??? static void Main(string[] args) {
?????? Heater heater = new Heater();
?????? Screen screen = new Screen();
?????? heater.Register(screen);???? // 注冊顯示器
?????? heater.BoilWater();???????????? // 燒水
??? }
}
輸出為:
ChinaXi'an - RealFire 001:
Alarm? :水快燒開了,當前溫度:98。
Alarm? :水快燒開了,當前溫度:99。
Alarm? :水已經燒開了!!
可以看到和前面完全一樣的輸出。
推模式和拉模式 的區別
那么大家一定想問,使用推模式和拉模式,有什么區別呢?
- 推模式的好處是 按需供給,想要提供給 Observer端什么數據,就將這些數據封裝成對象,傳遞給Observer,缺點是需要創建自定義的EventArgs對象。
- 拉模式的好處 則是不需要另外定義對象,直接將自身的引用傳遞進去就可以了。但是缺點是我們可能會需要暴露我們不想暴露的內部成員,比如本例中的temperature。我們期望將它作為類的內部數據,僅提供給顯示器。但是使用拉模式,你只得為它再提供一個公共的Temperature訪問器,這樣在程序的其他的地方也可以訪問到了,比如說在Program里。除此以外,我們不期望Screen可以進行燒水BoilWater()這一動作,但是由于它獲得了Heater的引用,而BoilWater()方法又是Public公共的,所以在Update()方法中也具備了對熱水器操作的能力,比如調用 BoilWater() 方法。
.Net 中沒有內置的IObserver和IObservable接口,因為在.Net中,可以通過委托和事件來完成,但是一樣面臨選擇推模式還是拉模式的問題,何時使用哪種策略完全依賴于設計者,你也可以將兩種方式都實現了,比如,將IObserver接口定義成這樣:
// 類似微軟的實現:兩個都用 ...
void Update(Object sender, BoiledEventArgs e);
注意,這里我用得是BoiledEventArgs作為Update()的參數,這里顯然不夠合適,如果期望這個接口可以為各種Observer服務,而不僅限于燒水這一事件,那么最好定義一個基類 EventArgs,然后對于各種不同的事件,定義不同的EventArgs類,再讓它們去繼承EventArgs。如此,可以得到下面的接口定義:
void Update(Object sender, EventArgs e);
呵呵,看到這里諸君應該都明白了吧,微軟對這個方法原型定義了一個委托,叫做EventHandler:
public delegate void EventHandler(object sender, EventArgs e);
再談下去又繞到委托和事件了,我們回到主題,將本文的內容做個總結吧。
總結
本文我再次使用熱水器的例子實現了Observer設計模式,但這一次我沒有使用委托和事件,而是通過經典的GOF方式。我同時還討論了實現Observer模式時Subject向Observer提供數據值可以采用的兩種方式--推模式和拉模式。最后,我們對這兩種模式進行了一個簡單的比較,并簡要介紹了.Net Framework中采用的方式。
感謝閱讀,希望這篇文章能給你帶來幫助!
轉載于:https://www.cnblogs.com/GeneralXU/archive/2009/09/09/1563117.html
總結
以上是生活随笔為你收集整理的重温Observer模式--热水器·改(转载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ASP.NET 2.0与SQL Expr
- 下一篇: 鼠标移动时,光标相对于对象的位置