关闭 x
IT技术网
    技 采 号
    ITJS.cn - 技术改变世界
    • 实用工具
    • 菜鸟教程
    IT采购网 中国存储网 科技号 CIO智库

    IT技术网

    IT采购网
    • 首页
    • 行业资讯
    • 系统运维
      • 操作系统
        • Windows
        • Linux
        • Mac OS
      • 数据库
        • MySQL
        • Oracle
        • SQL Server
      • 网站建设
    • 人工智能
    • 半导体芯片
    • 笔记本电脑
    • 智能手机
    • 智能汽车
    • 编程语言
    IT技术网 - ITJS.CN
    首页 » 算法设计 »三种观察者模式的C#实现

    三种观察者模式的C#实现

    2015-04-06 00:00:00 出处:卡奴达摩的专栏
    分享

    说起观察者模式,估计能搜出一堆来。因此写这篇博客的目的有两点:

    观察者模式是写松耦合代码的必备模式,重要性不言而喻,抛开代码层面,许多组件都采用了Publish-Subscribe模式,因此我想按照自己的理解重新设计一个使用场景并把观察者模式灵活使用在其中 我想把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 RunInBackgournd(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 api,调用起来代码更清晰:

    智能闹钟(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时,会覆盖牛奶加热器的函数,即每次只支持一个观察者使用。

    上一篇返回首页 下一篇

    声明: 此文观点不代表本站立场;转载务必保留本文链接;版权疑问请联系我们。

    别人在看

    正版 Windows 11产品密钥怎么查找/查看?

    还有3个月,微软将停止 Windows 10 的更新

    Windows 10 终止支持后,企业为何要立即升级?

    Windows 10 将于 2025年10 月终止技术支持,建议迁移到 Windows 11

    Windows 12 发布推迟,微软正全力筹备Windows 11 25H2更新

    Linux 退出 mail的命令是什么

    Linux 提醒 No space left on device,但我的空间看起来还有不少空余呢

    hiberfil.sys文件可以删除吗?了解该文件并手把手教你删除C盘的hiberfil.sys文件

    Window 10和 Windows 11哪个好?答案是:看你自己的需求

    盗版软件成公司里的“隐形炸弹”?老板们的“法务噩梦” 有救了!

    IT头条

    公安部:我国在售汽车搭载的“智驾”系统都不具备“自动驾驶”功能

    02:03

    液冷服务器概念股走强,博汇、润泽等液冷概念股票大涨

    01:17

    亚太地区的 AI 驱动型医疗保健:2025 年及以后的下一步是什么?

    16:30

    智能手机市场风云:iPhone领跑销量榜,华为缺席引争议

    15:43

    大数据算法和“老师傅”经验叠加 智慧化收储粮食尽显“科技范”

    15:17

    技术热点

    商业智能成CIO优先关注点 技术落地方显成效(1)

    用linux安装MySQL时产生问题破解

    JAVA中关于Map的九大问题

    windows 7旗舰版无法使用远程登录如何开启telnet服务

    Android View 事件分发机制详解

    MySQL用户变量的用法

      友情链接:
    • IT采购网
    • 科技号
    • 中国存储网
    • 存储网
    • 半导体联盟
    • 医疗软件网
    • 软件中国
    • ITbrand
    • 采购中国
    • CIO智库
    • 考研题库
    • 法务网
    • AI工具网
    • 电子芯片网
    • 安全库
    • 隐私保护
    • 版权申明
    • 联系我们
    IT技术网 版权所有 © 2020-2025,京ICP备14047533号-20,Power by OK设计网

    在上方输入关键词后,回车键 开始搜索。Esc键 取消该搜索窗口。