当前位置: 首页 > 编程笔记 >

C#中观察者模式的3种实现方式

宦瀚
2023-03-14
本文向大家介绍C#中观察者模式的3种实现方式,包括了C#中观察者模式的3种实现方式的使用技巧和注意事项,需要的朋友参考一下

说起观察者模式,估计在园子里能搜出一堆来。所以写这篇博客的目的有两点:

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();

//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<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下的三种观察者模型实现方案,能在编程场景下选择最合适的模型当然是我们的最终目标。本文提供下载本文章所使用的源码,如需转载请注明出处

 类似资料:
  • 本文向大家介绍c# 实现观察者模式,包括了c# 实现观察者模式的使用技巧和注意事项,需要的朋友参考一下 说明:主要参考《Head First设计模式(中文版)》,使用C#代码实现。 代码:Github 1、观察者模式UML图 2、气象监测类图 3、气象监测代码(书中C#版) 3.1 Observer 3.2 Subject 3.3 测试代码 4、使用C#中IObservable接口实现气象监测 4

  • 3.1. 模式动机 建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在此,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是观察者模式的模式动机。 3.2. 模式定义 观察者模式(Observer Pattern):定义对象间的

  • 模式定义 定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新 要点总结 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。 观察者自己决定是否需要订阅通知,目标对象对此一无所知。

  • 我想在Dart中实现一个观察者模式,但是我不知道如何去做。 假设我有一门课: 现在,每当我更改字段时,我都希望将“observed_field changed”字符串打印到控制台中。使用自定义设置器非常简单: 当然,如果我没有一个,而是许多这样的字段,我不想创建所有这些 getter 和 setter。显而易见的理论解决方案是将它们动态添加到类中,如下所示(不是工作代码,只是我希望它看起来如何的示

  • 主要内容:介绍,实现,Subject.java,Observer.java,BinaryObserver.java,OctalObserver.java,HexaObserver.java,ObserverPatternDemo.java当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。 介绍 意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知

  • 当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。 介绍 意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。 何时使用:一个对象(目标对象)