0

0

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

黄舟

黄舟

发布时间:2016-12-12 15:37:23

|

1548人浏览过

|

来源于php中文网

原创

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

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

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和IObserver实现观察者模式

IObservable 正如名称含义-可观察的事物,即主题(subject),Observer很明显就是观察者了。

在我们的场景中智能闹钟是IObservable,该接口只定义了一个方法IDisposable Subscribe(IObserver observer);该方法命名让人有点犯晕,Subscribe即订阅的意思,不同于之前提到过的观察者(observer)订阅主题(subject)。在这里是主题(subject)来订阅观察者(observer),其实这里也说得通,因为在该模型下,主题(subject)维护了一个观察者(observer)列表,所以有主题订阅观察者之说,我们来看闹钟的IDisposable Subscribe(IObserver observer)实现:

public IDisposable Subscribe(IObserver observer)
        {
            if (!_observers.Contains(observer))
            {
                _observers.Add(observer);
            }
            return new DisposedAction(() => _observers.Remove(observer));
        }

可以看到这里维护了一个观察者列表_observers,闹钟在到点了之后会遍历所有观察者列表将消息逐一通知给观察者

商易多用户商城
商易多用户商城

功能介绍:1. 商品出售包含拍卖模式,一口价模式。2. 全套系统采用淘宝网风格,成熟,简洁大方3. 每个商品支持多张图片上传,可自由设定,满足广大网民的迫切要求4. 商品信息支持 ubb,图文并茂5. 注册用户可参与竞拍,或者拍卖自己的商品6. 拥有会员注册,交易提醒,成交商品确认等邮件发送功能7. 拥有交易双方信用评价的功能,使得交易安全可*,可信度高8. 拥有安全稳定的用户虚拟币平台,可实现商

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

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

9

2026.01.27

拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

107

2026.01.26

edge浏览器怎样设置主页 edge浏览器自定义设置教程
edge浏览器怎样设置主页 edge浏览器自定义设置教程

在Edge浏览器中设置主页,请依次点击右上角“...”图标 > 设置 > 开始、主页和新建标签页。在“Microsoft Edge 启动时”选择“打开以下页面”,点击“添加新页面”并输入网址。若要使用主页按钮,需在“外观”设置中开启“显示主页按钮”并设定网址。

13

2026.01.26

苹果官方查询网站 苹果手机正品激活查询入口
苹果官方查询网站 苹果手机正品激活查询入口

苹果官方查询网站主要通过 checkcoverage.apple.com/cn/zh/ 进行,可用于查询序列号(SN)对应的保修状态、激活日期及技术支持服务。此外,查找丢失设备请使用 iCloud.com/find,购买信息与物流可访问 Apple (中国大陆) 订单状态页面。

120

2026.01.26

npd人格什么意思 npd人格有什么特征
npd人格什么意思 npd人格有什么特征

NPD(Narcissistic Personality Disorder)即自恋型人格障碍,是一种心理健康问题,特点是极度夸大自我重要性、需要过度赞美与关注,同时极度缺乏共情能力,背后常掩藏着低自尊和不安全感,影响人际关系、工作和生活,通常在青少年时期开始显现,需由专业人士诊断。

6

2026.01.26

windows安全中心怎么关闭 windows安全中心怎么执行操作
windows安全中心怎么关闭 windows安全中心怎么执行操作

关闭Windows安全中心(Windows Defender)可通过系统设置暂时关闭,或使用组策略/注册表永久关闭。最简单的方法是:进入设置 > 隐私和安全性 > Windows安全中心 > 病毒和威胁防护 > 管理设置,将实时保护等选项关闭。

6

2026.01.26

2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】
2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】

铁路12306提供起售时间查询、起售提醒、购票预填、候补购票及误购限时免费退票五项服务,并强调官方渠道唯一性与信息安全。

112

2026.01.26

个人所得税税率表2026 个人所得税率最新税率表
个人所得税税率表2026 个人所得税率最新税率表

以工资薪金所得为例,应纳税额 = 应纳税所得额 × 税率 - 速算扣除数。应纳税所得额 = 月度收入 - 5000 元 - 专项扣除 - 专项附加扣除 - 依法确定的其他扣除。假设某员工月工资 10000 元,专项扣除 1000 元,专项附加扣除 2000 元,当月应纳税所得额为 10000 - 5000 - 1000 - 2000 = 2000 元,对应税率为 3%,速算扣除数为 0,则当月应纳税额为 2000×3% = 60 元。

32

2026.01.26

oppo云服务官网登录入口 oppo云服务登录手机版
oppo云服务官网登录入口 oppo云服务登录手机版

oppo云服务https://cloud.oppo.com/可以在云端安全存储您的照片、视频、联系人、便签等重要数据。当您的手机数据意外丢失或者需要更换手机时,可以随时将这些存储在云端的数据快速恢复到手机中。

101

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

c语言项目php解释器源码分析探索
c语言项目php解释器源码分析探索

共7课时 | 0.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号