首頁 > 後端開發 > C#.Net教程 > C#中觀察者模式的3種實現方式

C#中觀察者模式的3種實現方式

黄舟
發布: 2016-12-12 15:37:23
原創
1400 人瀏覽過

說起觀察者模式,估計在園子裡能搜出一堆。所以寫這篇部落格的目的有兩點:

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 Alarm;

2.為主題(subject)的信息定義一個EventArgs,即AlarmEventArgs,這裡麵包含了事件所有的信息

3.主題(subject)通過以下方式發出事件

var args = new AlarmEventArgs(_alarmTime.Value, 0.92m);
 OnAlarmEvent(args);
登入後複製
Event

OnAlarmarm方法的定義

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 是 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和IObserver實作觀察者模式

IObservable 如同名稱意義-可觀察的事物,即主題(subject),Observer很明顯就是觀察者了。

在我們的場景中智慧鬧鐘是IObservable,該介面只定義了一個方法IDisposable Subscribe(IObserver observer);這個方法命名讓人有點犯暈,Subscribe即訂閱的意思,不同於之前提到過的觀察者(observer)訂閱主題(subject)。這裡是主題(subject)來訂閱觀察者(observer),其實這裡也說得通,因為在該模型下,主題(subject)維護了一個觀察者(observer)列表,所以有主題訂閱觀察者之說,我們來看鬧鐘的IDisposable Subscribe(IObserver 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();
//Act
milkSchedule.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,闹钟在到点后直接执行该Action即可:

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下的三种观察者模型实现方案,能在编程场景下选择最合适的模型当然是我们的最终目标。


相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板