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

前言

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

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

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

  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)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

工业自动化领域常见的通讯协议

工业自动化领域常见的通讯协议&#xff0c;包括PROFINET、PROFIBUS、Modbus、Ethernet/IP、CANopen、DeviceNet和BACnet。通过分析这些协议的技术特点、应用场景及优势&#xff0c;比较它们在工业自动化中的性能和适用性&#xff0c;帮助选择最合适的协议以优化系统性能和可靠性…

记录AE快捷键(持续补充中。。。)

记录AE快捷键 快捷键常用快捷键图层快捷键工具栏图层与属性常用指令视图菜单时间轴常规快捷键项目首选项功能摄像机操作 常用操作导入AI/PS工程文件加选一个关键参数快速回到上下一帧隐藏/显示图层关键帧拉长缩短关键帧按着鼠标左键不松手&#xff0c;在秒表那一列往下移动会都…

为什么电源滤波器中的电容器太大

所有 AC-DC 转换器&#xff0c;无论是线性电源还是具有某种开关元件&#xff0c;都需要一种机制来获取交流侧的变化功率并在直流侧产生恒定功率。通常&#xff0c;大滤波电容器用于在交流功率高于直流负载所需时吸收和存储能量&#xff0c;并在交流功率低于所需时向负载提供能量…

常用的JDK调优监控工具整理

JVM 调优首先要做的就是监控 JVM 的运行状态&#xff0c;这就需要用到各种官方和第三方的工具包了 一、 JDK 工具包 JDK 自带的 JVM 工具可以分为命令行工具和可视化工具 命令行工具 jps: JVM Process status tool&#xff1a;JVM进程状态工具&#xff0c;查看进程基本信息j…

DomoAI让你轻松变身视频达人!支持20s完整视频生成!

账号注册 官网&#xff1a;https://www.domoai.app/zh-Hant/library 功能 支持不同风格的视频类型&#xff0c;支持图片转视频&#xff0c;支持文字转图片&#xff0c;支持静态图片变为动态。 可以切换语言为中文 风格转换 选择不同风格的 支持生成20s&#xff0c;目前接触…

0. 云原生之基于乌班图远程开发

云原生专栏大纲 文章目录 安装乌班图配置静态IP重置root密码开启root远程登录开启远程SSH访问安装docker安装docker-compose安装Edge浏览器安装搜狗输入法安装TeamViewer安装虚拟显示器安装JDK安装maven安装vscodevscode插件安装VSCode配置maven、git、jdk、自动报错vscode快捷…

2024年【陕西省安全员C证】考试及陕西省安全员C证最新解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 陕西省安全员C证考试参考答案及陕西省安全员C证考试试题解析是安全生产模拟考试一点通题库老师及陕西省安全员C证操作证已考过的学员汇总&#xff0c;相对有效帮助陕西省安全员C证最新解析学员顺利通过考试。 1、【多…

树以及二叉树的定义和特点

目录 开场白 树的定义 结点的分类 结点间的关系 树的其他相关概念 树的存储结构 孩子兄弟表示法 二叉树的定义 二叉树的特点 特殊二叉树 满二叉树 完全二叉树 二叉树的性质 二叉树的存储结构 开场白 这一篇文章是关于树的知识&#xff0c;这是一个比较特…

Python 学习 用Python第二册 第9章内容解八皇后问题

----用教授的方法学习 目录 1.八皇后问题 2.状态表示(抽象) 3.检测冲突 4.基线条件 5.递归条件 6.结尾 1.八皇后问题 深受大家喜爱的计算机科学谜题&#xff1a;你需要将8个皇后放在棋盘上&#xff0c;条件是任何一个皇后都不能威胁其他皇后&#xff0c;即任何两个皇后…

利用485缓存器实现两主一丛RS485串行通信

作者:艺捷自动化&#xff0c;其旗下产品有艺捷自动化网站和易为二维码小程序&#xff08;微信&#xff09; 对于工控自动化领域的电气工程师来说&#xff0c;基于RS485的串行通讯是最常见的。绝大部分仪表都能支持这种通讯方式。RS485通讯&#xff0c;是一种异步半双工模式&…

誉天5月红帽战报:恭喜14名学员通过RHCE认证,通过率87.5%!

红帽认证是全球公认的Linux权威认证之一&#xff0c;对于Linux从业者来说具有很高的价值和认可度。旨在评估考生在Linux系统管理和应用方面的专业知识和技能。红帽考试是Linux从业者提升自身技能水平和职业竞争力的重要途径之一。 5月份&#xff0c;誉天14名学员通过了RHCE认证…

css入门宝典

3.1.4 通配符选择器 语法 : *{} 作用 : 让页面中所有的标签执行该样式,通常用来清除间距 例子 : *{ margin: 0; //外间距 padding: 0; //内间距 } 一 CSS基本语法 1基础知识 1.1概述 Css (层叠样式表)是种格式化网页的标准方式&#xff0c; 用于控制设置网页的样式&#xff…

WSL Ubuntu安装TensorFlow-GPU、PyTorch-GPU

在Windows 11的WSL Ubuntu中安装TensorFlow-GPU、PyTorch-GPU 0、WSL Ubuntu安装 在Windows 11的商店中下载即可&#xff0c;此处以Ubuntu22.04.3为例 1、CUDA Toolkit安装 参考公孙启的文章Windows11 WSL Ubuntu Pycharm Conda for deeplearning前往nVidia官网下载CUDA …

transformer模型首次体验代码

前言 首先是安装python&#xff0c;更新pip源到清华源。安装transformer pip install transformer安装jupyter lab&#xff0c;也简单一行 pip install jupyterlab现在不想用anaconda了&#xff0c;因为国内没有源了&#xff0c;国外的又慢。直接用pip吧。 然后开始体验之旅…

DeepDriving | CUDA编程-05:流和事件

本文来源公众号“DeepDriving”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;CUDA编程-05&#xff1a;流和事件 1 CUDA流 在CUDA中有两个级别的并发&#xff1a;内核级并发和网格级并发。前面的文章DeepDriving | CUDA编程-04&…

buildroot编译出错you should not run configure as root

虚拟机版本&#xff1a;ubuntu-22.04.4 问题 buildroot在图形配置后&#xff0c;执行 sudo make开始编译出现以下错误configure: error: you should not run configure as root (set FOenvironment to bypass this check) 在网上看到说在/etc/profile文件中添加以下内容 exp…

Ngunx + Tomcat 负载均衡和动态分离

目录 一、tomcat简介 二、Nginx 负载均衡 1. Nginx 应用 2. Nginx 负载均衡实现原理 2.1 正向代理 2.2 反向代理 2.3 具体过程接收请求&#xff1a;Nginx作为反向代理服务器&#xff0c;接收客户端的请求。选择后端服务器&#xff1a;根据预先配置的负载均衡算法&#xf…

23种设计模式之享元模式

享元模式 1、定义 享元模式&#xff1a;运用共享技术有效的支持大量细粒度对象的复用 2、享元模式结构 Flyweight&#xff08;抽象享元类&#xff09;&#xff1a;通常是一个接口或抽象类&#xff0c;在抽象享元类中声明了具体享元类公共的方法&#xff0c;这些方法可以向外…

从多线程设计模式到对 CompletableFuture 的应用

大家好&#xff0c;我是 方圆。最近在开发 延保服务 频道页时&#xff0c;为了提高查询效率&#xff0c;使用到了多线程技术。为了对多线程方案设计有更加充分的了解&#xff0c;在业余时间读完了《图解 Java 多线程设计模式》这本书&#xff0c;觉得收获良多。本篇文章将介绍其…

几种经典查找算法

几种经典查找算法 顺序查找法二分查找法判定树 二叉查找树&#xff08;BST&#xff09;索引查找B-树B树散列表&#xff08;hash&#xff09;查找 顺序查找法 顺序查找的平均查找长度为&#xff1a; 时间复杂度为0&#xff08;n&#xff09;&#xff1b; 二分查找法 int bin…