流動的觀察者模式 | Flutter 設計模式

觀察者模式,又稱發佈訂閱模式,是一種行為設計模式——你可以定義一種訂閱機制,可在對象事件發生時通知多個 觀察 該對象的其他對象。

觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象。

這個主題對象在狀態上發生變化時,會通知所有觀察者對象,讓它們能夠自動更新自己。

從定義中,不難發現,觀察者被觀察者 / 發佈者 是這個模式中最重要的組成元素。

微信的公眾號可以被視為生活中最典型的觀察者模式的例子。如果你訂閱了「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 方法都會被調用。) ,重建子樹,達到局部更新狀態的目的。

路由是在 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 路由中又完成了這個非常重要的任務。

本文小結

本文內容到這裡就結束了,觀察者模式的場景例子數不勝數,在實際開發中,我們也會經常需要使用到,但我們要記住的是設計模式的運用並不是套用模版,而是要根據實際場景找到最合適的解決方案。

對於行為型模式來說,觀察者模式將被觀察者與觀察者這兩件事物抽象出來,實現了代碼上的解藕,在實際場景中,觀察者可能是關心某種狀態的組件,監聽某個事件的監聽器等等,整體的設計也會變得更加直觀,希望大家能在以後的開發中多多使用。

拓展閱讀

關於本系列文章

Flutter / Dart 設計模式從南到北 (簡稱 Flutter 設計模式) 系列內容由 CFUG 社區成員、《Flutter 開發之旅從南到北》作者、小米工程師楊加康撰寫並發佈在 Flutter 社區公眾號和 flutter.cn 網站的社區教程欄目。

本系列預計兩周發佈一篇,着重向開發者介紹 Flutter 應用開發中常見的設計模式以及開發方式,旨在推進 Flutter / Dart 語言特性的普及,以及幫助開發者更高效地開發出高質量、可維護的 Flutter 應用。

Tags: