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

news2024/11/24 0:09:21

前言

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

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

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

  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 name
  void addObserver(Observer observer, String name) {
    center.addObserver(observer, name);
  }

  ///  Remove observer for notification name
  ///
  /// @param observer - who will receive notification
  /// @param name     - notification name
  void 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 info
  Future<void> postNotification(String name, dynamic sender, [Map? info]) async {
    await center.postNotification(name, sender, info);
  }

  ///  Post a notification
  ///
  /// @param notification - notification object
  Future<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 {

  // ...

  @override
  Future<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');
  }

  @override
  void dispose() {
    var nc = lnc.NotificationCenter();
    nc.removeObserver(this, 'DocumentUpdated');
    nc.removeObserver(this, 'ContactsUpdated');
    super.dispose();
  }

  // ...

}

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

    // post notification
    var 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 name
  void 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 name
  void 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 name
      Set<Observer>? listeners = _observers[name];
      if (listeners != null && listeners.remove(observer)) {
        // observer removed
        if (listeners.isEmpty) {
          _observers.remove(name);
        }
      }
    }
  }

  ///  Post notification with name
  ///
  /// @param name     - notification name
  /// @param sender   - notification sender
  /// @param info     - extra info
  Future<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 finished
    await 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;

  @override
  String 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 name
  void addObserver(Observer observer, String name) {
    center.addObserver(observer, name);
  }

  ///  Remove observer for notification name
  ///
  /// @param observer - who will receive notification
  /// @param name     - notification name
  void 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 info
  Future<void> postNotification(String name, dynamic sender, [Map? info]) async {
    await center.postNotification(name, sender, info);
  }

  ///  Post a notification
  ///
  /// @param notification - notification object
  Future<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 name
  void 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 name
  void 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 name
      Set<Observer>? listeners = _observers[name];
      if (listeners != null && listeners.remove(observer)) {
        // observer removed
        if (listeners.isEmpty) {
          _observers.remove(name);
        }
      }
    }
  }

  ///  Post notification with name
  ///
  /// @param name     - notification name
  /// @param sender   - notification sender
  /// @param info     - extra info
  Future<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 finished
    await Future.wait(tasks);
  }

}

GitHub 地址:

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

结语

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

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

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

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

相关文章

如何用 Google Chrome 浏览器浏览经过 XSLT 渲染的XML 文件

对于经过 XSLT 渲染的XML 文件&#xff0c;本来&#xff0c;可以直接用 IE (Internet Explorer) 打开&#xff0c;就能看到渲染之后的样子&#xff0c;很方便。但是后来&#xff0c;微软把 IE 换成了 Microsoft Edge&#xff0c;按理说这是比 IE 更先进的浏览器&#xff0c;可是…

centos7系统使用docker-compose安装部署jenkins

CentOS7系统使用docker-compose安装部署jenkins&#xff0c;并实现前后端自动构建 记录一次在给公司部署jenkins的真实经历&#xff0c;总结了相关经验 1.准备环境 1.java 由于最新的jenkins需要jdk11以上才能支持&#xff0c;而系统里的jdk是1.8的&#xff0c;因此等jenkins…

qmt交易框架2.0版本----支持实时高频交易

经过了2个星期的开发&#xff0c;终于迎来了qm交易框架2.0&#xff0c;超过了3000行源代码&#xff0c;使用类开发&#xff0c;使用方便。 我们看看利用框架写一个简单的实时高频交易策略&#xff0c;很简单 源代码 from qmt_trader.qmt_trader import qmt_traderfrom qmt_tr…

工业 web4.0 的 UI 卓越非凡

工业 web4.0 的 UI 卓越非凡

求导,积分

求导公式&#xff1a; 复合函数求导法则&#xff1a;两个函数导函数的乘积. 例如&#xff1a;f(x)2x1,f(x)2,g(x)x^24x4,g(x)2x4 那么复合函数&#xff1a; g(f(x))(2x1)^24(2x1)4 把&#xff08;2x1&#xff09;看做整体,则g2(2x1)4 然后再求&#xff08;2x1&#xff09;的导函…

代码随想录算法训练营刷题复习1 :动态规划背包问题 01背包+完全背包

动态规划刷题复习 一、01背包 416. 分割等和子集1049. 最后一块石头的重量 II494. 目标和474. 一和零 416. 分割等和子集 class Solution { public:bool canPartition(vector<int>& nums) {int sum0;for(int i0;i<nums.size();i) {sumnums[i];}if(sum%2!0)retu…

Tuple 元组

文章目录 一、什么是元组 &#xff1f;二、元组的具体操作2.1 创建元组2.1.1 tuple() 创建元组函数和 list() 创建列表函数总结 2.2 元组的元素访问操作2.3 元组的元素计数操作2.4 zip 对象 一、什么是元组 &#xff1f; 列表属于可变序列,可以任意修改列表中的元素。 元组的…

JUC并发编程-第二天:线程池相关

线程池相关 线程池内置线程池的使用线程池的关闭excute方法和submit方法的区别 线程池 线程池就是一个可以复用线程的技术 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,Thread…

HTML静态网页成品作业(HTML+CSS)——游戏永劫无间网页(3个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有3个页面。 二、作品演示 三、代…

初见 Rollup 的十大常见问题

文章目录 初见 Rollup 的十大常见问题1. 超神奇的 Rollup 英文解释&#xff01;2. 为什么 ESM 要比 CommonJS 要好呢&#xff1f;3. 什么是 tree-shaking ?4. 如何使用 Rollup 处理 CommonJS&#xff1f;5. 为什么 node-resolve 不是一个内置功能&#xff1f;6. 为什么在进行代…

网络协议,OSI,简单通信,IP和mac地址

认识协议 1.讲故事 2004年&#xff0c;小明因为给他爹打电话&#xff08;座机&#xff09;费用太贵&#xff0c;所以约定一种信号&#xff0c;响一次是报平安&#xff0c;响两次是要钱&#xff0c;响三次才需要接通。 2.概念 协议&#xff1a;是一种约定&#xff0c;这种约…

如何实现电脑监视员工的电脑屏幕?六个方法偷偷分享给你

实现电脑监视员工的电脑屏幕&#xff0c;通常需要借助专业的监控软件或系统&#xff0c;这些工具旨在帮助企业管理者监督员工的工作状态&#xff0c;确保工作效率&#xff0c;同时保护公司资产和数据安全。以下是几种常见的实现方式。 1. 使用专业的远程监控软件 安企神软件&a…

kaggle竞赛实战10——特征优化

特征优化思路&#xff1a; 在完成常规流程后&#xff0c;如果不知道怎么办&#xff0c;可以针对文本or时间序列特征进行进一步处理 首先&#xff0c;我们注意到&#xff0c;每一笔信用卡的交易记录都有交易时间&#xff0c;而对于时间字段和文本字段&#xff0c;普通的批量创…

解决用Three.js实现嘴型和语音同步时只能播放部分部位的问题 Three.js同时渲染播放多个组件变形动画的方法

前言 参考这篇文章ThreeJSChatGPT 实现前端3D数字人AI互动&#xff0c;前面搭后端、训练模型组内小伙伴都没有什么问题&#xff0c;到前端的时候&#xff0c;脸部就出问题了。看我是怎么解决的。 好文章啊&#xff0c;可惜百度前几个都找不到&#xff0c;o(╥﹏╥)o 问题情况 …

【C++】环境搭建及基本工作流程

C 当你需要写性能良好的代码时&#xff0c;C仍是不二选择 如果你想访问硬件、如果你想对硬件进行控制&#xff0c;C仍是首选。 所有的游戏引擎都是用C编写的&#xff0c;因为C可以直接控制硬件。 原理&#xff1a; C代码-->编译器编译comple-->目标平台的机器码-->放…

[算法刷题—二分法]寻找插入位置

题目展示: 本道题本身并不是很难,主要是学习和分析二分查找插入位置的方法。 首先大体上分为两种情况: 一.target在待查找的数组之中,返回对应值的下标索引。 二.target不在待查找的数组之中&#xff0c;需要返回target插入位置的索引(原数组有序) 第一种情况不难&#xff…

视图-什么是(VIEW)?怎么创建(CREATE VIEW)?怎么删除(DROP)?怎么用(SELECT/INSERT/UPDATE/DELETE)?

一、引言 之前对数据库的操作都是针对基本关系表&#xff0c;操作都是在数据库的全局逻辑模式上进行的&#xff0c;而在实际的数据库系统中&#xff0c;可能用户只关心或只被允许使用数据库中的某些基本关系表或基本关系表中的某些属性列&#xff0c;这些数据构成了数据库的外…

基于SVD的点云配准(下)

点云配准及特征提取详细解读 本篇博客将介绍一个用于点云配准的 C++ 代码示例,该示例使用 PCL(Point Cloud Library)库来处理和配准两个点云数据集。我们将逐步解析代码的关键部分,并解释每个步骤的作用。 代码说明 代码的整体结构及其主要功能: int main(int argc, ch…

Vue 状态管理:从Vuex到Pinia,Vue 3官方推荐的状态管理库深度解析

大家好&#xff0c;我是前端宝哥。 在编程界有句老话&#xff1a;“命名和缓存失效是世上两大难题。” 我得说&#xff0c;在现代Web应用的状态管理上&#xff0c;这难题得排第三&#xff01; 今天&#xff0c;咱们来深挖一下Vue的状态管理之道&#xff0c;并介绍一个超直观的解…

汽车IVI中控开发入门及进阶(二十八):视频SERDES芯片

前言: SerDes不是很常见,SerDes是将Ser和Des两种产品组合在一起的名称。Ser是Serializer或“并串转换器”的缩写,Des是Deserializer或“串并转换器”的简写。 Serdes是不是必须的?上一节介绍了camera,上上节也研究了video decoder,那么带摄像头的应用应该具体选哪个方案…