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

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

Dec 12, 2016 pm 03:37 PM

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

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


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1670
14
CakePHP 教程
1428
52
Laravel 教程
1329
25
PHP教程
1276
29
C# 教程
1256
24
c#.net的持續相關性:查看當前用法 c#.net的持續相關性:查看當前用法 Apr 16, 2025 am 12:07 AM

C#.NET依然重要,因為它提供了強大的工具和庫,支持多種應用開發。 1)C#結合.NET框架,使開發高效便捷。 2)C#的類型安全和垃圾回收機制增強了其優勢。 3).NET提供跨平台運行環境和豐富的API,提升了開發靈活性。

從網絡到桌面:C#.NET的多功能性 從網絡到桌面:C#.NET的多功能性 Apr 15, 2025 am 12:07 AM

C#.NETisversatileforbothwebanddesktopdevelopment.1)Forweb,useASP.NETfordynamicapplications.2)Fordesktop,employWindowsFormsorWPFforrichinterfaces.3)UseXamarinforcross-platformdevelopment,enablingcodesharingacrossWindows,macOS,Linux,andmobiledevices.

C#作為多功能.NET語言:應用程序和示例 C#作為多功能.NET語言:應用程序和示例 Apr 26, 2025 am 12:26 AM

C#在企業級應用、遊戲開發、移動應用和Web開發中均有廣泛應用。 1)在企業級應用中,C#常用於ASP.NETCore開發WebAPI。 2)在遊戲開發中,C#與Unity引擎結合,實現角色控制等功能。 3)C#支持多態性和異步編程,提高代碼靈活性和應用性能。

C#.NET與未來:適應新技術 C#.NET與未來:適應新技術 Apr 14, 2025 am 12:06 AM

C#和.NET通過不斷的更新和優化,適應了新興技術的需求。 1)C#9.0和.NET5引入了記錄類型和性能優化。 2).NETCore增強了雲原生和容器化支持。 3)ASP.NETCore與現代Web技術集成。 4)ML.NET支持機器學習和人工智能。 5)異步編程和最佳實踐提升了性能。

將C#.NET應用程序部署到Azure/AWS:逐步指南 將C#.NET應用程序部署到Azure/AWS:逐步指南 Apr 23, 2025 am 12:06 AM

如何將C#.NET應用部署到Azure或AWS?答案是使用AzureAppService和AWSElasticBeanstalk。 1.在Azure上,使用AzureAppService和AzurePipelines自動化部署。 2.在AWS上,使用AmazonElasticBeanstalk和AWSLambda實現部署和無服務器計算。

C#和.NET運行時:它們如何一起工作 C#和.NET運行時:它們如何一起工作 Apr 19, 2025 am 12:04 AM

C#和.NET運行時緊密合作,賦予開發者高效、強大且跨平台的開發能力。 1)C#是一種類型安全且面向對象的編程語言,旨在與.NET框架無縫集成。 2).NET運行時管理C#代碼的執行,提供垃圾回收、類型安全等服務,確保高效和跨平台運行。

c#和.net:了解兩者之間的關係 c#和.net:了解兩者之間的關係 Apr 17, 2025 am 12:07 AM

C#和.NET的關係是密不可分的,但它們不是一回事。 C#是一門編程語言,而.NET是一個開發平台。 C#用於編寫代碼,編譯成.NET的中間語言(IL),由.NET運行時(CLR)執行。

C#.NET開發:入門的初學者指南 C#.NET開發:入門的初學者指南 Apr 18, 2025 am 12:17 AM

要開始C#.NET開發,你需要:1.了解C#的基礎知識和.NET框架的核心概念;2.掌握變量、數據類型、控制結構、函數和類的基本概念;3.學習C#的高級特性,如LINQ和異步編程;4.熟悉常見錯誤的調試技巧和性能優化方法。通過這些步驟,你可以逐步深入C#.NET的世界,並編寫高效的應用程序。

See all articles