流动的观察者模式 | Flutter 设计模式

发表于 2年以前  | 总阅读数:2056 次

观察者模式,又称发布订阅模式,是一种行为设计模式——你可以定义一种订阅机制,可在对象事件发生时通知多个 观察 该对象的其他对象。

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。

这个主题对象在状态上发生变化时,会通知所有观察者对象,让它们能够自动更新自己。

从定义中,不难发现,观察者被观察者 / 发布者 是这个模式中最重要的组成元素。

微信的公众号可以被视为生活中最典型的观察者模式的例子。如果你订阅了「Flutter社区」,每当 Flutter 社区发布文章时,就会给你及其他订阅者推送这个消息,这其中你就是 观察者,公众号「Flutter社区」就是 被观察者 (Observable) 或发布者 (Subject)

观察者模式经常被应用在这类事件处理系统中,从概念上理解,被观察者也经常被称作是 事件流 (stream of events) 或者说是 事件流的来源 (stream source of events),而观察者相当于 事件接收器 (sinks of events)

同时,观察者模式也是实现 响应式编程 的基础,RxDart、EventBus 等库都是观察者模式下的产物。

面向对象

面向对象中,观察者和和发布者 (被观察者) 分别对应两个类 (Observer 和 Subject) 的对象。

观察者模式 UML 图,图源维基百科

发布类 (Subject) 中通常会有提供给每个对象订阅或取消订阅发布者事件流的 订阅机制,包括:

  1. 一个用于存储订阅者对象引用的列表成员变量;
  2. 几个用于添加或删除该列表中订阅者的公有方法。
// 被观察者
class Subject {
  List<Observer> _observers;
  Subject([List<Observer> observers]) {
    _observers = observers ?? [];
  }

  // 注册观察者
  void registerObserver(Observer observer) {
    _observers.add(observer);
  }

  // 解注册观察者
  void unregisterObserver(Observer observer) {
    _observers.remove(observer)
  }

  // 通知观察者
  void notifyobservers(Notification notification) {
    for (var observer in _observers) {
      observer.notify(notification);
    }
  }
}

此时,每当事件发生,它只需遍历订阅者并调用其对象的特定通知方法即可 (如上面代码中的 notifyobservers 方法) 。

实际应用中,一个发布者通常会对应多个订阅者,且发布者与订阅者应当遵循面向对象的开发设计原则,因此:

  1. 为了避免耦合,订阅者们必须实现同样的接口;
  2. 发布者仅通过该接口与订阅者交互,接口方法可以声明参数, 这样发布者在发出通知时就能传递一些上下文数据 (如下面代码中的 notification 对象) 。
// 观察者
class Observer {
  String name;

  Observer(this.name);

  void notify(Notification notification) {
    print("[${notification.timestamp.toIso8601String()}] Hey $name, ${notification.message}!");
  }
}

这样,我们可以得出如下这样用 Dart 语言实现的观察者模式了,下面是一个简单的应用:

// 具体的被观察者 CoffeeMaker
// 每当 Coffee 制作完成发出通知给观察者。
class CoffeeMaker extends Subject {
  CoffeeMaker([List<Observer> observers]) : super(observers);

  void brew() {
    print("Brewing the coffee...");
    notifyobservers(Notification.forNow("coffee's done"));
  }
}

void main() {
  var me = Observer("Tyler");
  var mrCoffee = CoffeeMaker(List.from([me]));
  var myWife = Observer("Kate");
  mrCoffee.registerObserver(myWife);
  mrCoffee.brew();
}

这里的 CoffeeMaker 继承自 Subject,作为一个具体的发布类,brew() 方法是其内部,每当咖啡制作完成后,用于通知其他各个观察者的方法。上面代码中,我们在 mrCoffee 这台咖啡机上注册了 myWife 这一个观察者,mrCoffee.brew(); 触发后,myWife 内部的 notify 方法就会被调用。

观察者模式很好的实现了他们两者之间发布订阅的关系,在实际应用中,被观察者正在处理的事件很可能是异步的,而作为观察者不必显示的去阻塞等待事件的完成,而是由被观察者通知,当事件完成后,再将事件主动地「推」给关心这个事件的观察者。与之相对的,有一类观察者也会使用后台线程时刻轮询地监听着其关心的主题事件,这个话题我们暂不展开。

观察者模式使用不慎的话,也很容易出现传说中的 失效监听器 问题,导致内存泄漏,因为在基本实现中,被观察者依然持有观察者的强引用,如果事件中途,被观察者已经不存在时或不再关心此事件,就会导致观察者无法被回收,因此,我们在这种情况下应当在被观察中做好取消订阅的机制,及时释放无用的资源。

Dart

Stream 可以被看作是 Dart 语言原生支持的观察者模式的典型模型之一,它本身是 Dart:async 包中一个用于异步操作的类,响应式编程库 RxDart 也是基于 Stream 封装而成的。

从概念上讲,我们可以将 Stream 看做是一个可以连接两端的传送带,作为开发者,我们可以在传送带的一端放入数据,Stream 就会将这些数据传送到另一端。

和现实中的情况类似,如果传送带的另一端没有人接受数据,这些数据就会被程序丢弃,因此,我们通常会在传送到尾端安排一个接收数据的对象,在响应式编程中,它被称为数据的观察者。

如果说上文 Dart 面向对象中,观察者和被观察者两者的关系是在尽量保持低耦合的情况下而形成的,相对独立。那么在响应式编程中,它们的关系就是变得更加紧密的 上游与下游 的关系。

因为 Stream,顾名思义,就是「流」的含义,被观察者在流的入口产生事件,观察者则在流的出口等待数据或事件的到来。

在这套流程里,观察者的 订阅 与被观察者的 事件发布 等一系列操作都直接在 Stream 或者说是框架内部完成的。

Dart 中,我们可以使用 StreamController 来创建流:

var controller = new StreamController<int>();

controller.add(1); // 将数据放入流中

如上面代码所示,创建 StreamController 时必须指定泛型类型来定义可以加入 Stream 的数据对象,上面的 controller 可以接受 int 类型的数据,我们使用它的 add 方法就可以将数据放入到它的传送带中。

如果我们直接运行上面的两行代码,最终并不会不到任何结果,因为我们还没有为传送带设置接收数据的对象:

var controller = new StreamController<int>();

controller.stream.listen((item) => print(item)); // 数据观察者函数

controller.add(1);
controller.add(2);
controller.add(3);

上面的代码中,我们通过调用 StreamController 内部的 stream 对象的 listen 方法,就可以为 controller 对象添加监听这个 Stream 事件的观察者,这个方法接受一个回调函数,这个回调函数又接受一个我们在 new StreamController<int>() 泛型中声明的数据对象作为参数。

这时,每当我们再次通过 add 方法将数据放入传送带后,就会通知观察者,调用这个函数,并将传递的数据打印出来:

1
2
3

另外,我们也可以使观察者在某个时间段后停止监听 Stream 中传递的数据,在上面代码中的 listen 函数会返回一个 StreamSubscription 类型的订阅对象,当我们调用它的 .cancel() 后就会释放这个观察者,不再接收数据:

var controller = new StreamController<String>();

StreamSubscription subscription = controller.stream.listen((item) => print(item));

controller.add(1);
controller.add(2);
controller.add(3);

await Future.delayed(Duration(milliseconds: 500));

subscription.cancel();

Flutter

ChangeNotifier

ChangeNotifier 大概是 Flutter 中实现观察者模式最典型的例子了,它实现自 Listenable,内部维护一个 _listeners 列表用来存放观察者,并实现了 addListenerremoveListener 等方法来完成其内部的订阅机制:

class ChangeNotifier implements Listenable {
  LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>();

  @protected
  bool get hasListeners {
    return _listeners!.isNotEmpty;
  }

  @override
  void addListener(VoidCallback listener) {
    _listeners!.add(_ListenerEntry(listener));
  }

  @override
  void removeListener(VoidCallback listener) {
    for (final _ListenerEntry entry in _listeners!) {
      if (entry.listener == listener) {
        entry.unlink();
        return;
      }
    }
  }

  @mustCallSuper
  void dispose() {
    _listeners = null;
  }

  @protected
  @visibleForTesting
  void notifyListeners() {
    if (_listeners!.isEmpty)
      return;
    final List<_ListenerEntry> localListeners = List<_ListenerEntry>.from(_listeners!);
    for (final _ListenerEntry entry in localListeners) {
      try {
        if (entry.list != null)
          entry.listener();
      } catch (exception, stack) {
        // ...
      }
    }
  }
}

在实际使用时,我们只需要继承 ChangeNotifier 便能具备这种订阅机制,如下这个 CartModel 类:

class CartModel extends ChangeNotifier {
  final List<Item> _items = [];

  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  int get totalPrice => _items.length * 42;

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }

  void removeAll() {
    _items.clear();
    notifyListeners();
  }
}

CartModel 内部维护一个 _items 数组,addremoveAll 方法时提供给外部操作该数组的接口,每当 _items 改变则会调用 notifyListeners() 通知它的所有观察者。

ChangeNotifier 作为 flutter:foundation 中最基础的类,不依赖其他任何上层的类,测试起来也非常简单,我们可以针对 CartModel 做一个简单的单元测试:

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});

这里,当我们调用 cart.add(Item('Dash')); 后,就是会触发观察者函数的调用,实现一种由数据的改变驱动事件执行的机制。

Flutter 应用中最传统的状态管理方案是使用有状态 widget 的 setState 的方法,这种方式暴露出来的问题是,大型应用中的 widget 树会非常复杂,每当状态更新调用 setState 时,则会牵一发而动全身,重建所有子树,使性能大打折扣。

那么,当将 ChangeNotifier 观察者模式应用在状态管理方案中时,便能解决这个问题。设想让每一个最小组件充当观察者,观察应用的状态,每当状态改变时即驱动该局部小组件更新,是不是就能达到这种目的。我们常用 provider 库就应用了这个原理。

provider 内部提供了一个 ChangeNotifierProvider widget,可以向其子组件暴露一个 ChangeNotifier 实例 (被观察者) :

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

在子组件中,只需要使用 Consumer widget 注册观察者组件,就能接收到 CartModel 内部数据更新的通知:

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);

这里,使用 Consumer 必须指定要观察的 ChangeNotifier 类型,我们要访问 CartModel 那么就写上 Consumer<CartModel>,builder 最为 Consumer 唯一一个必要参数,用来构建展示在页面中的子组件。

ChangeNotifier 发生变化的时候会调用 builder 这个函数。(换言之,当调用 CartModelnotifyListeners() 方法时,所有相关的 Consumer widget 的 builder 方法都会被调用。) ,重建子树,达到局部更新状态的目的。

Navigator

路由是在 Flutter 应用中常去讨论的话题,在整个应用运行过程中,路由操作也都需要被时刻关注着,它是我们了解用户行为的一种有效的方式。Flutter 提供了一套很方便的观察者模式的模型帮助我们实现这个功要求。

Flutter 中每个 Navigator 对象都接受一个 NavigatorObserver 对象的数组,在实际开发过程中,我们可以通过根组件 MaterialApp (或 CupertinoPageRoute) 的 navigatorObservers 属性传递给根 Navigator 组件,用于观察根 Navigator 的路由行为,这一组 NavigatorObserver 对象就是一系列的路由观察者。

 Widget build(BuildContext context) {
    return new MaterialApp(
      navigatorObservers: [new MyNavigatorObserver()],
      home: new Scaffold(
        body: new MyPage(),
      ),
    );
  }

路由观察者们统一继承自 RouteObserver,范型类型为 PageRoute,这时,它就能监听 CupertinoPageRoute 和 MaterialPageRoute 两种类型的路由了:

class MyRouteObserver extends RouteObserver<PageRoute<dynamic>> {

  // 监听导航器的 push 操作
  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    super.didPush(route, previousRoute);
    if (previousRoute is PageRoute && route is PageRoute) {
      print('${previousRoute.settings.name} => ${route.settings.name}');
    }
  }

  // 监听导航器的 replace 操作
  @override
  void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
    super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
    if (newRoute is PageRoute) {
      print('${oldRoute.settings.name} => ${oldRoute.settings.name}');
    }
  }

  // 监听导航器的 pop 操作
  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
    super.didPop(route, previousRoute);
    if (previousRoute is PageRoute && route is PageRoute) {
      print('${route.settings.name} => ${previousRoute.settings.name}');
    }
  }
}

在我们做实际路由操作,调用 Navigatorpoppush 等方法时,就会按照惯例遍历调用这些观察者对象对应的方法:

 Future<T> push<T extends Object>(Route<T> route) {
  // ...
  for (NavigatorObserver observer in widget.observers)
    observer.didPush(route, oldRoute);
 // ...
}

这样,观察者模式在 Flutter 路由中又完成了这个非常重要的任务。

本文小结

本文内容到这里就结束了,观察者模式的场景例子数不胜数,在实际开发中,我们也会经常需要使用到,但我们要记住的是设计模式的运用并不是套用模版,而是要根据实际场景找到最合适的解决方案。

对于行为型模式来说,观察者模式将被观察者与观察者这两件事物抽象出来,实现了代码上的解藕,在实际场景中,观察者可能是关心某种状态的组件,监听某个事件的监听器等等,整体的设计也会变得更加直观,希望大家能在以后的开发中多多使用。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/cDETnQpYkin1qpBWjUW8eg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
如何有效定位Flutter内存问题? 3年以前  |  14793次阅读
Flutter的手势GestureDetector分析详解 4年以前  |  10891次阅读
Flutter插件详解及其发布插件 4年以前  |  10665次阅读
在Flutter中添加资源和图片 5年以前  |  7922次阅读
 目录