面向事件编程之观察者模式

前言

村里的老人常说:真男人就该懂得遵守“三不原则”——不主动、不拒绝、不负责

一个复杂的软件系统,其中必然会存在各种各样的“对象”,如果在设计之初没有注意控制好耦合度,导致各个对象甚至是函数之间高度耦合,那对于后期开发和维护将是一个灾难!

在日常开发中,大家不难发现,“面向事件编程”是解耦合的利器,其对应的设计模式便是大家常常会听到的“观察者模式”,而核心思想,就是尽可能令大部分对象都遵守“三不原则”:

  1. 合理设计事件处理器,等待事件的发生,而不要主动轮询某个临界资源;
  2. 设置一个安全高效的事件分发中心,无论多大并发都能保证不拒绝服务
  3. 事件生产者不必关心事件将由谁来处理、如何处理,亦无需对结果负责

接下来我将为大家展示如何设计一个优雅的本地消息分发处理系统。

接口设计

首先我们定义一个通用的 ```Notification``` (也可以叫“事件”,或者“消息”),它包含3个基本信息:

  1. 事件名;
  2. 发送者;
  3. 当前信息(上下文)
///  Notification object with name, sender and extra info
class Notification {Notification(this.name, this.sender, this.userInfo);final String name;final dynamic sender;final Map? userInfo;}

然后我们定义“观察者”接口,以便让任务中心正确分发消息:

///  Notification observer
abstract class Observer {Future<void> onReceiveNotification(Notification notification);}

由于消费者处理该事件时有可能会需要花费较长时间,所以这里我们设计为异步接口。

最后我们再实现一个消息分发中心:

///  Singleton
class NotificationCenter {factory NotificationCenter() => _instance;static final NotificationCenter _instance = NotificationCenter._internal();NotificationCenter._internal();BaseCenter center = BaseCenter();///  Add observer with notification name////// @param observer - who will receive notification/// @param name     - notification namevoid addObserver(Observer observer, String name) {center.addObserver(observer, name);}///  Remove observer for notification name////// @param observer - who will receive notification/// @param name     - notification namevoid removeObserver(Observer observer, [String? name]) {center.removeObserver(observer, name);}///  Post a notification with extra info////// @param name   - notification name/// @param sender - who post this notification/// @param info   - extra infoFuture<void> postNotification(String name, dynamic sender, [Map? info]) async {await center.postNotification(name, sender, info);}///  Post a notification////// @param notification - notification objectFuture<void> post(Notification notification) async {await center.post(notification);}}

这个事件分发中心主要实现3个功能:

  1. 将一个观察者以及其关心的事件名称添加到内部等候队列;
  2. 将一个观察者移出等候队列;
  3. 提交一个事件(中心内部异步执行分发)。

并且,因为一个应用系统中通常应该只有一个事件分发中心,所以这里的 NotificationCenter 被设计成为单例模式。

这样一个通用的本地消息分发系统就设计完成了。

应用示例

接下来将为你展示这个系统如何使用。

首先我们先定义一个观察者,并且将其添加到事件分发中心:

第一步,实现 Observer 接口:

import 'package:lnc/notification.dart' as lnc;class _ContactListState extends State<ContactListPage> implements lnc.Observer {// ...@overrideFuture<void> onReceiveNotification(lnc.Notification notification) async {// 获取事件名称与相关信息String name = notification.name;Map? userInfo = notification.userInfo;// 根据事件名称处理信息if (name == 'ContactsUpdated') {ID? contact = userInfo?['contact'];Log.info('contact updated: $contact');// ...} else if (name == 'DocumentUpdated') {ID? did = userInfo?['ID'];Log.info('document updated: $did');// ...}}}

第二步,在适当的时机将该观察者添加进事件分发中心 or 从中心删除:

class _ContactListState extends State<ContactListPage> implements lnc.Observer {_ContactListState() {// ...var nc = lnc.NotificationCenter();nc.addObserver(this, 'ContactsUpdated');nc.addObserver(this, 'DocumentUpdated');}@overridevoid dispose() {var nc = lnc.NotificationCenter();nc.removeObserver(this, 'DocumentUpdated');nc.removeObserver(this, 'ContactsUpdated');super.dispose();}// ...}

第三步,在事件发生点提交事件给分发中心:

    // post notificationvar nc = NotificationCenter();nc.postNotification('DocumentUpdated', this, {'ID': identifier,'document': doc,});

至此,一个观察者模式的本地事件系统的应用就介绍完了。

下面我们再来深入一下内部,看看这个事件分发中心是如何实现的?

进阶

注意前面提到的事件分发中心(单例) NotificationCenter,里面有一个代理对象 center:

///  Singleton
class NotificationCenter {factory NotificationCenter() => _instance;static final NotificationCenter _instance = NotificationCenter._internal();NotificationCenter._internal();BaseCenter center = BaseCenter();  // 代理对象,内部实现(可替换)// ...}

这里采用代理模式,是为了方便用户根据项目的特殊需要,定义具体的分发逻辑实现以替换之。

下面介绍一下这个默认的分发中心 BaseCenter:

class BaseCenter {// name => WeakSet<Observer>final Map<String, Set<Observer>> _observers = {};///  Add observer with notification name////// @param observer - listener/// @param name     - notification namevoid addObserver(Observer observer, String name) {Set<Observer>? listeners = _observers[name];if (listeners == null) {listeners = WeakSet();  // 弱引用集合listeners.add(observer);_observers[name] = listeners;} else {listeners.add(observer);}}///  Remove observer from notification center////// @param observer - listener/// @param name     - notification namevoid removeObserver(Observer observer, [String? name]) {if (name == null) {// 1. remove from all observer set_observers.forEach((key, listeners) {listeners.remove(observer);});// 2. remove empty set_observers.removeWhere((key, listeners) => listeners.isEmpty);} else {// 3. get listeners by nameSet<Observer>? listeners = _observers[name];if (listeners != null && listeners.remove(observer)) {// observer removedif (listeners.isEmpty) {_observers.remove(name);}}}}///  Post notification with name////// @param name     - notification name/// @param sender   - notification sender/// @param info     - extra infoFuture<void> postNotification(String name, dynamic sender, [Map? info]) async {return await post(Notification(name, sender, info));}Future<void> post(Notification notification) async {Set<Observer>? listeners = _observers[notification.name]?.toSet();if (listeners == null) {return;}List<Future> tasks = [];for (Observer item in listeners) {tasks.add(item.onReceiveNotification(notification));}// wait all tasks finishedawait Future.wait(tasks);}}
  1. 首先,它有3个接口函数和 NotificationCenter 一一对应:
  2. 其次,它的内部有一个 key 为字符串的映射对象 _observers,其中每一个事件名称(字符串)映射向一个弱引用的集合 WeakSet,集合中的元素则是关注该事件名称的所有观察者;
  3. 当生产者提交事件时,该中心会根据该事件名称从 _observers 中获取对应的观察者集合,并调用其事件接口函数。

这里有两点值得注意:

  1. 由于低耦合的设计,各个观察者(事件消费者)分别独立处理事件结果,相互之间并无关联,并且也没有前后时序关系的要求,所以这里的 post 函数会采用异步并发的方式来同时调用这些观察者接口;
  2. 一般而言,观察者的添加与移除是一一对应的,但为了防止异常情况发生,这里的观察者集合仍然采用弱引用的集合,以便某些观察者非正常退出时,即使没有显式调用 removeObserver() 函数,也不会造成泄漏。

(关于弱引用的实现我们留到以后再来讲解)

代码引用

由于我已提交了一个完整的模块代码到 pub.dev,所以在实际应用中,你只需要在项目工程文件 ```pubspec.yaml``` 中添加

dependencies:lnc: ^0.1.2

然后在需要使用的 dart 文件头引入即可:

import 'package:lnc/notification.dart' as lnc;

全部源码

import 'package:object_key/object_key.dart';
import 'package:lnc/log.dart';///  Notification observer
abstract class Observer {Future<void> onReceiveNotification(Notification notification);
}///  Notification object with name, sender and extra info
class Notification {Notification(this.name, this.sender, this.userInfo);final String name;final dynamic sender;final Map? userInfo;@overrideString toString() {Type clazz = runtimeType;return '<$clazz name="$name">\n\t<sender>$sender</sender>\n''\t<info>$userInfo</info>\n</$clazz>';}}///  Notification center
class NotificationCenter {factory NotificationCenter() => _instance;static final NotificationCenter _instance = NotificationCenter._internal();NotificationCenter._internal();BaseCenter center = BaseCenter();///  Add observer with notification name////// @param observer - who will receive notification/// @param name     - notification namevoid addObserver(Observer observer, String name) {center.addObserver(observer, name);}///  Remove observer for notification name////// @param observer - who will receive notification/// @param name     - notification namevoid removeObserver(Observer observer, [String? name]) {center.removeObserver(observer, name);}///  Post a notification with extra info////// @param name   - notification name/// @param sender - who post this notification/// @param info   - extra infoFuture<void> postNotification(String name, dynamic sender, [Map? info]) async {await center.postNotification(name, sender, info);}///  Post a notification////// @param notification - notification objectFuture<void> post(Notification notification) async {await center.post(notification);}}class BaseCenter with Logging {// name => WeakSet<Observer>final Map<String, Set<Observer>> _observers = {};///  Add observer with notification name////// @param observer - listener/// @param name     - notification namevoid addObserver(Observer observer, String name) {Set<Observer>? listeners = _observers[name];if (listeners == null) {listeners = WeakSet();listeners.add(observer);_observers[name] = listeners;} else {listeners.add(observer);}}///  Remove observer from notification center////// @param observer - listener/// @param name     - notification namevoid removeObserver(Observer observer, [String? name]) {if (name == null) {// 1. remove from all observer set_observers.forEach((key, listeners) {listeners.remove(observer);});// 2. remove empty set_observers.removeWhere((key, listeners) => listeners.isEmpty);} else {// 3. get listeners by nameSet<Observer>? listeners = _observers[name];if (listeners != null && listeners.remove(observer)) {// observer removedif (listeners.isEmpty) {_observers.remove(name);}}}}///  Post notification with name////// @param name     - notification name/// @param sender   - notification sender/// @param info     - extra infoFuture<void> postNotification(String name, dynamic sender, [Map? info]) async {return await post(Notification(name, sender, info));}Future<void> post(Notification notification) async {Set<Observer>? listeners = _observers[notification.name]?.toSet();if (listeners == null) {logDebug('no listeners for notification: ${notification.name}');return;}List<Future> tasks = [];for (Observer item in listeners) {try {tasks.add(item.onReceiveNotification(notification).onError((error, st) =>Log.error('observer error: $error, $st, $notification')));} catch (ex, stackTrace) {logError('observer error: $ex, $stackTrace, $notification');}}// wait all tasks finishedawait Future.wait(tasks);}}

GitHub 地址:

https://github.com/dimchat/sdk-dart/blob/main/lnc/lib/src/notification.dart

结语

这里展示了一个基由观察者模式设计的本地事件通知分发系统,其中包含了“观察者模式”、“单例模式”、“代理模式”等设计思想,希望对你有帮助。

如有其他问题,可以下载登录 Tarsier 与我交流(默认通讯录里找 Albert Moky)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/27856.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络安全知识全景地图V1.0 - 20240616更新

网络安全领域的知识全景涵盖了从基础概念到高级技术的广泛内容。博主基于自身十年多的工作经验结合CISSP认证官方教材按照不同的主题和层次梳理出如下高层次的概览地图&#xff0c;可以帮助个人和组织理解网络安全领域的主题。 1.1. 基础理论 1.1.1. 网络安全概述 网络安全的…

React@16.x(29)useRef

目录 1&#xff0c;介绍2&#xff0c;和 React.createRef() 的区别3&#xff0c;计时器的问题 目前来说&#xff0c;因为函数组件每次触发更新时&#xff0c;都会重新运行。无法像类组件一样让一些内容保持不变。 所以才出现了各种 HOOK 函数&#xff1a;useState&#xff0c;u…

Camtasia Studio 2024软件最新版下载【安装详细图文教程】

​Camtasia是美国TechSmith公司出品的一款集电脑屏幕录制、视频剪辑为一体的软件套装。同时包含Camtasia 录制器、Camtasia Studio&#xff08;编辑器&#xff09;、Camtasia 菜单制作器、Camtasia 剧场、Camtasia 播放器和Screencast的内置功能。 安 装 包 获 取 地 址&#x…

AirPlay技术规范及认证资讯

AirPlay是Apple开发的一种无线技术&#xff0c;允许用户将音频、视频或图片从iOS设备、Mac电脑或其他支持AirPlay的设备无线传输到支持AirPlay的接收器设备上&#xff0c;例如智能电视或音响系统。这项技术基于Wi-Fi网络&#xff0c;提供了一种便捷的方式来共享媒体内容。AirPl…

车票信息的请求与显示

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 1 发送与分析车票信息的查询请求 得到了获取车票信息的网络请求地址&#xff0c;然后又分析出请求地址的必要参数以及车站名称转换的文件&#xff…

《软件测试52讲》——测试基础知识篇

1 你真的懂测试吗&#xff1f;从“用户登录”测试谈起 从“用户登录”测试谈起&#xff0c;“用户登录”功能作为测试对象 作为测试工程师&#xff0c;你的目标是要保证系统在各种应用场景下的功能是符合设计要求的&#xff0c;所以你需要考虑的测试用例就需要更多、更全面。 …

2078.两栋颜色不同且距离最远的房子

街上有 n 栋房子整齐地排成一列&#xff0c;每栋房子都粉刷上了漂亮的颜色。给你一个下标从 0 开始且长度为 n 的整数数组 colors &#xff0c;其中 colors[i] 表示第 i 栋房子的颜色。 返回 两栋 颜色 不同 房子之间的 最大 距离。 第 i 栋房子和第 j 栋房子之间的距离是 a…

Linux应用编程 - i2c-dev操作I2C

嵌入式Linux操作I2C设备&#xff0c;我们一般会在内核态编写I2C驱动程序。另外还能在用户空间编写I2C程序&#xff0c;下面介绍相关代码的实现。 i2c-dev框架在内核中封装了I2C通信所需要的所有通信细节&#xff0c;I2C适配器会在/dev目录下创建字符设备&#xff0c;例如&#…

kettle从入门到精通 第七十课 ETL之kettle kettle数据校验,脏数据清洗轻松拿捏

场景&#xff1a;输入在指定的错误(错误应涵盖数据类型不匹配的情况)行数内&#xff0c;trans不报错&#xff0c;但通过错误处理步骤捕捉&#xff0c;并记入文件&#xff0c;整个数据管线正常完成直至处理完最后一个输入行。 解决方案&#xff1a;使用步骤【数据检验】进行处理…

本周MoonBit新增Wasm1引用计数支持、语法即将添加错误恢复机制

MoonBit更新 【Wasm MVP】Wasm1 后端添加基于 Perceus 算法的引用计数支持 【语法】throw raise try catch 均被保留为关键字 为了即将添加的错误处理机制 【Core】List与sorted_map被移动至core/immut下 List被移动至core/immut/list包中&#xff0c;并被移除内置类型支持 …

哈希表、递归在二叉树中的应用-1372. 二叉树中的最长交错路径

题目链接及描述 1372. 二叉树中的最长交错路径 - 力扣&#xff08;LeetCode&#xff09; 题目分析 题目所述&#xff0c;计算在二叉树中交替遍历的最大深度【左->右->左】【右->左->右】&#xff0c;例如对于从当前根节点root出发&#xff0c;则此时遍历方向有两个…

今日成果2024-6-7 TrustZone TEE安全SDK开发指南

Rockchip Vendor Storage Application Note.pdf OK 开机下&#xff0c;可以实现Vendor Storage的读写。 0ms时同步RTC时间 OK Rockchip_Developer_Guide_TEE_SDK_CN.pdf 什么是TrustZone 此系统方法意味着可以保护安全内存、加密块、键盘和屏幕等外设&#xff0c;从而可确…

视频剪辑可以赚钱吗 快速学会视频剪辑的方法

由于视频剪辑的需求不断增长&#xff0c;学会视频剪辑成为一项自媒体必备的技能&#xff0c;这个技能可以为个人带来收入和职业发展带来机会。无论是作为自由职业者还是在公司工作&#xff0c;掌握视频剪辑技能都可以为你提供更多的工作机会和竞争优势。这篇文章将讲解视频剪辑…

2.深度学习-线性回归

文章目录 环境配置&#xff08;必看&#xff09;线性回归代码工程运行结果1.对比图2.运行结果 环境配置&#xff08;必看&#xff09; Anaconda-创建虚拟环境的手把手教程相关环境配置看此篇文章&#xff0c;本专栏深度学习相关的版本和配置&#xff0c;均按照此篇文章进行安装…

Carsim高级开发:VS Connect通讯开发指南

文章目录 前言一、VS Connect 概念引入二、VS Connect 通讯框架三、Carsim 工程配置1、车辆模型配置2、procedure配置3、Run Control配置4、受控车辆名称配置 四、VS Connect Server代码1、打开Sln工程2、代码修改 五、VS Connect Client代码1、函数的调用关系2、carsim_variab…

5G消息 x 文旅 | 一站式智慧文旅解决方案

5G消息 x 文旅 | 一站式智慧文旅解决方案 文旅 x 5G 消息将进一步强化资源整合&#xff0c;满足游客服务需求、企业营销需求、政府管理需求&#xff0c;推进文化旅游项目的智慧化、数字化&#xff0c;增强传播力、竞争力和可持续性。5G 消息的“原生入口”、“超强呈现”、“智…

帕金森病的食疗建议

帕金森病&#xff08;PD&#xff09;是一种慢性、进展性的神经退行性疾病&#xff0c;主要影响中老年人。虽然目前尚无法根治&#xff0c;但及早规范治疗可显著改善症状&#xff0c;提高患者的生活质量。饮食调理作为帕金森病综合治疗的重要组成部分&#xff0c;对于维持患者较…

接口测试详解

接口测试详解 本文主要讲软件接口 一、什么是接口&#xff1f;硬件接口&#xff1a;硬件接口指的是硬件提供给外界的一种实体。主要作用是内部数据分离出外 部的沟通方法 目的是&#xff1a;沟通外部来改变内部的数据。如&#xff1a;USB接口&#xff0c;投影仪接口 软件接口…

【CDN】逆天 CDN !BootCDN 向 JS 文件中植入恶意代码

今天在调试代码&#xff0c;突然控制台出现了非常多报错。 这非常可疑&#xff0c;报错指向的域名也证实了这一点。 因为我的 HTML 中只有一个外部开源库&#xff08;qrcode.min.js&#xff09;&#xff0c;因此只有可能是它出现了问题。 我翻看了请求记录&#xff0c;发现这…

【无线感知】【P3】无线感知手势识别-Ubicomp2022论文分享

前言&#xff1a; 本篇主要关于手势识别的方向的,主要参考 北京大学-《无线感知手势识别-Ubicomp2022论文分享》 目录&#xff1a; 技术背景 主要问题&#xff08;异质性问题&#xff09; 感知模型 EDP DPSense 实现效果 一 技术背景 基于WIFI的手势识别在智能家具,以…