說起觀察者模式,估計在園子裡能搜出一堆來。所以寫這篇部落格的目的有兩點:
1.觀察者模式是寫松耦合代碼的必備模式,重要性不言而喻,拋開代碼層面,許多組件都採用了Publish-Subscribe模式,所以我想按照自己的理解重新設計一個使用情境並把觀察者模式靈活使用在其中
2.我想把C#中實現觀察者模式的三個方案做一個總結,目前還沒看到這樣的總結
現在我們來假設這樣的一個情境,並利用觀察者模式實現需求:
未來智能家居進入了每家每戶,每個家居都留有API供客戶進行自訂整合,所以第一個智能鬧鐘(smartClock)先登場,廠家為此鬧鐘提供了一組API,當設定一個鬧鈴時間後該鬧鐘會在此時做出通知,我們的智能牛奶加熱器,麵包烘烤機,擠牙膏裝置都要訂閱此鬧鐘鬧鈴訊息,自動為主人準備好牛奶,麵包,牙膏等。
這個情境是很典型觀察者模式,智能鬧鐘的鬧鈴是一個主題(subject),牛奶加熱器,麵包烘烤機,擠牙膏裝置是觀察者(observer),他們只需要訂閱這個主題即可實現松耦合的編碼模型。讓我們通過三種方案逐一實現此需求。
一、利用.net的Event模型來實現
.net中的Event模型是一種典型的觀察者模式,在.net出身之後被大量應用在了代碼當中,我們看事件模型如何在此種情境下使用,
首先介紹下智能鬧鐘,廠家提供了一組很簡單的API
public void SetAlarmTime(TimeSpan timeSpan) { _alarmTime = _now().Add(timeSpan); RunBackgourndRunner(_now, _alarmTime); }
SetAlarmTime(TimeSpan timeSpan)用來定時,當使用者佈建好一個時間後,鬧鐘會在後台跑一個類似於while(true)的迴圈對比時間,當鬧鈴時間到了後要發出一個通知事件出來
protected void RunBackgourndRunner(Func<DateTime> now,DateTime? alarmTime ) { if (alarmTime.HasValue) { var cancelToken = new CancellationTokenSource(); var task = new Task(() => { while (!cancelToken.IsCancellationRequested) { if (now.AreEquals(alarmTime.Value)) { //鬧鈴時間到了 ItIsTimeToAlarm(); cancelToken.Cancel(); } cancelToken.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(2)); } }, cancelToken.Token, TaskCreationOptions.LongRunning); task.Start(); } }
其他代碼並不重要,重點在當鬧鈴時間到了後要執行ItIsTimeToAlarm(); 我們在這裡發出事件以便通知訂閱者,.net中實現event模型有三要素,
1.為主題(subject)要定義一個event, public event Action<Clock, AlarmEventArgs> Alarm;
2.為主題(subject)的資訊定義一個EventArgs,即AlarmEventArgs,這裡麵包含了事件所有的資訊
3.主題(subject)通過以下方式發出事件
var args = new AlarmEventArgs(_alarmTime.Value, 0.92m); OnAlarmEvent(args);
OnAlarmEvent方法的定義
public virtual void OnAlarm(AlarmEventArgs e) { if(Alarm!=null) Alarm(this,e); }
這裡要注意命名,事件內容-AlarmEventArgs,事件-Alarm(動詞,例如KeyPress),觸發事件的方法 void OnAlarm(),這些元素都要符合事件模型的命名規範。
智能鬧鐘(SmartClock)已經實現完畢,我們在牛奶加熱器(MilkSchedule)中訂閱這個Alarm訊息:
public void PrepareMilkInTheMorning() { _clock.Alarm += (clock, args) => { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( args.AlarmTime, args.ElectricQuantity*100); Console.WriteLine(Message); }; _clock.SetAlarmTime(TimeSpan.FromSeconds(2)); }
在麵包烘烤機中同樣可以用_clock.Alarm+=(clock,args)=>{//it is time to roast bread}訂閱鬧鈴訊息。
至此,event模型介紹完畢,實現過程還是有點繁瑣的,並且事件模型使用不當會有memory leak的問題,當觀察者(obsever)訂閱了一個生命週期較長的主題(該主題生命週期長於觀察者),該觀察者並不會被記憶體回收(因為還有引用指主題),詳見Understanding and Avoiding Memory Leaks with Event Handlers and Event Aggregators,開發人員需要顯示退訂該主題(-=)。
園子裡老A也寫過一篇如何利用弱引用解決該問題的部落格:如何解決事件導致的Memory Leak問題:Weak Event Handlers。
二、利用.net中IObservable<out T>和IObserver<in T>實現觀察者模式
IObservable<out T> 正如名稱含義-可觀察的事物,即主題(subject),Observer很明顯就是觀察者了。
在我們的情境中智能鬧鐘是IObservable,該介面只定義了一個方法IDisposable Subscribe(IObserver<T> observer);該方法命名讓人有點犯暈,Subscribe即訂閱的意思,不同於之前提到過的觀察者(observer)訂閱主題(subject)。在這裡是主題(subject)來訂閱觀察者(observer),其實這裡也說得通,因為在該模型下,主題(subject)維護了一個觀察者(observer)列表,所以有主題訂閱觀察者之說,我們來看鬧鐘的IDisposable Subscribe(IObserver<T> observer)實現:
public IDisposable Subscribe(IObserver<AlarmData> observer) { if (!_observers.Contains(observer)) { _observers.Add(observer); } return new DisposedAction(() => _observers.Remove(observer)); }
可以看到這裡維護了一個觀察者列表_observers,鬧鐘在到點了之後會遍曆所有觀察者列表將訊息逐一通知給觀察者
public override void ItIsTimeToAlarm() { var alarm = new AlarmData(_alarmTime.Value, 0.92m); _observers.ForEach(o=>o.OnNext(alarm)); }
很明顯,觀察者有個OnNext方法,方法簽名是一個AlarmData,代表了要通知的訊息資料,接下來看看牛奶加熱器的實現,牛奶加熱器作為觀察者(observer)當然要實現IObserver介面
public void Subscribe(TimeSpan timeSpan) { _unSubscriber = _clock.Subscribe(this); _clock.SetAlarmTime(timeSpan); } public void Unsubscribe() { _unSubscriber.Dispose(); } public void OnNext(AlarmData value) { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( value.AlarmTime, value.ElectricQuantity * 100); Console.WriteLine(Message); }
除此之外為了方便使用麵包烘烤器,我們還加了兩個方法Subscribe()和Unsubscribe(),看調用過程
var milkSchedule = new MilkSchedule();//ActmilkSchedule.Subscribe(TimeSpan.FromSeconds(12));
三、Action函數式方案
在介紹該方案之前我需要說明,該方案並不是一個觀察者模型,但是它卻可以實現同樣的功能,並且使用起來更簡練,也是我最喜歡的一種用法。
這種方案中,智能鬧鐘(smartClock)提供的API需要設計成這樣:
public void SetAlarmTime(TimeSpan timeSpan,Action<AlarmData> alarmAction) { _alarmTime = _now().Add(timeSpan); _alarmAction = alarmAction; RunBackgourndRunner(_now, _alarmTime); }
方法簽名中要接受一個Action<T>,鬧鐘在到點後直接執行該Action<T>即可:
public override void ItIsTimeToAlarm() { if (_alarmAction != null) { var alarmData = new AlarmData(_alarmTime.Value, 0.92m); _alarmAction(alarmData); } }
牛奶加熱器中使用這種API也很簡單:
_clock.SetAlarmTime(TimeSpan.FromSeconds(1), (data) => { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( data.AlarmTime, data.ElectricQuantity * 100); });
在實際使用過程中我會把這種API設計成fluent模型,調用起來代碼更清晰:
智能鬧鐘(smartClock)中的API:
public Clock SetAlarmTime(TimeSpan timeSpan) { _alarmTime = _now().Add(timeSpan); RunBackgourndRunner(_now, _alarmTime); return this; } public void OnAlarm(Action<AlarmData> alarmAction) { _alarmAction = alarmAction; }
牛奶加熱器中進行調用:
_clock.SetAlarmTime(TimeSpan.FromSeconds(2)) .OnAlarm((data) => { Message = "Prepraring milk for the owner, The time is {0}, the electric quantity is {1}%".FormatWith( data.AlarmTime, data.ElectricQuantity * 100); });
顯然改進後的寫法語義更好:鬧鐘.設定鬧鈴時間().當警示時(()=>{執行以下功能})
這種函數式寫法更簡練,但是也有明顯的缺點,該模型不支援多個觀察者,當麵包烘烤機使用這樣的API時,會覆蓋牛奶加熱器的函數,即每次只支援一個觀察者使用。
結束語,本文總結了.net下的三種觀察者模型實現方案,能在編程情境下選擇最合適的模型當然是我們的最終目標。