Flutter(able) 的單例模式

文/ 楊加康,CFUG 社群成員,《Flutter 開發之旅從南到北》作者,小米工程師

單例設計模式(Singleton Design Pattern)理解起來非常簡單。

一個類只允許建立一個實例,那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。

作為最簡單的一種設計模式之一,對於單例本身的概念,大家一看就能明白,但在某些情況下也很容易使用不恰當。相比其他語言,Dart 和 Flutter 中的單例模式也不盡相同,本篇文章我們就一起探究看看它在 Dart 和 Flutter 中的應用。

Flutter(able) 的單例模式

一般來說,要在程式碼中使用單例模式,結構上會有下面這些約定俗成的要求:

  • 單例類(Singleton)中包含一個引用自身類別的靜態屬性實例(instance),且能自行建立這個實例。
  • 該實例只能透過靜態方法 getInstance() 訪問。
  • 類建構式函式通常沒有引數,且被標記為私有,確保不能從類外部實例化該類別。

單例設計模式 UML 圖,圖源:https://www.uml-diagrams.org/class-reference.html

遵循以上這些要求,我們就不難能用 Dart 寫出一個普通的單例模式:

class Singleton {
  static Singleton _instance;
  
  // 私有的命名建構式函式
  Singleton._internal();
  
  static Singleton getInstance() {
    if (_instance == null) {
      _instance = Singleton._internal();
    }
    
    return _instance;
  }
}

同時,在實現單例模式時,也需要考慮如下幾點,以防在使用過程中出現問題:

  • 是否需要延遲載入,即類別實例只在第一次需要時建立。
  • 是否執行緒安全,在 Java、C++ 等多執行緒語言中需要考慮到多執行緒的併發問題。由於 Dart 是單線程模型的語言,所有的程式碼通常都執行在同一個 isolate 中,因此不需要考慮執行緒安全的問題。
  • 在某些情況下,單例模式會被認為是一種 反模式,因為它違反了 SOLID 原則中的單一責任原則,單例類自己控制了自己的建立和生命週期,且單例模式一般沒有介面,擴充套件困難。
  • 單例模式的使用會影響到程式碼的可測試性。如果單例類依賴比較重的外部資源,比如 DB,我們在寫單元測試的時候,希望能透過 mock 的方式將它替換掉。而單例類這種硬編碼式的使用方式,導致無法實現 mock 替換。

在實際編碼過程中,單例模式常見應用有:

  • 全域日誌的 Logger 類、應用全域的設定資料物件類,單業務管理類。
  • 建立實例時佔用資源較多,或實例化耗時較長的類。
  • 等等…

Dart 化

如上文所說的,Dart 語言作為單線程模型的語言,實現單例模式時,我們本身已經可以不用再去考慮 執行緒安全 的問題了。Dart 的很多其他屬性也依然可以幫助到我們實現更加 Dart 化的單例。

使用 getter 運算子,可以打破單例模式中既定的,一定要寫一個 getInstance() 靜態方法的規則,簡化我們必須要寫的模版化程式碼,如下的 get instance:

class Singleton {
  static Singleton _instance;
  static get instance {
    if (_instance == null) {
      _instance = Singleton._internal();
    }
    
    return _instance;
  }
  
  Singleton._internal();
}

Dart 的 getter 的使用方式與普通方法大致相同,只是呼叫者不再需要使用括號,這樣,我們在使用時就可以直接使用如下方式拿到這個單例物件:

final singleton = Singleton.instance;

而 Dart 中特有的 工廠建構式函式(factory constructor)也原生具備了 不必每次都去建立新的類別實例 的屬性,將這個屬性利用起來,我們就可以寫出更優雅的 Dart(able) 單例模式了,如下:

class Singleton {
  static Singleton _instance;
  
  Singleton._internal();
  
  // 工廠建構式函式
  factory Singleton() {
    if (_instance == null) {
      _instance = Singleton._internal();
    }
    
    return _instance;
  }
}

這裡我們不再使用 getter 運算子額外提供一個函式,而是將單例物件的生成交給工廠建構式函式,此時,工廠建構式函式僅在第一次需要時建立 _instance,並之後每次回傳相同的實例。這時,我們就可以像下面這樣使用普通建構式函式的方式獲取到單例了:

final singleton = Singleton();

如果你還掌握了 Dart 空安全及箭頭函式等屬性,那麼還可以使用另一種方式進一步精簡程式碼,寫出像下面這樣 Dart 風味十足的程式碼:

class Singleton {
  static Singleton _instance;

  Singleton._internal() {
    _instance = this;
  }

  factory Singleton() => _instance ?? Singleton._internal();
}

這裡,使用 ?? 作為 _instance 實例的判空操作符,如果為空則呼叫建構式函式實例化否則直接回傳,也可以達到單例的效果。

以上,Dart 單例中延遲載入的無不是使用判空來實現的(if (_instance == null)??),但是在 Dart 空安全屬性裡還有一個非常重要的運算子 late ,它在語言層面就實現了實例的延遲載入,如下面這個例子:

class Singleton {
  Singleton._internal();
  
  factory Singleton() => _instance;
  
  static late final Singleton _instance = Singleton._internal();
}

被標記為 late 的變數 _instance 的初始化操作將會延遲到欄位首次被訪問時執行,而不是在類載入時就初始化。這樣,Dart 語言特有的單例模式的實現方式就這麼產生了。

Flutter 化

說到工廠建構式函式/空安全運算子等 Dart 語法上的屬性,Flutter 應用中的例子已經屢見不鮮了,但光看單例模式的定義,我們還必須聯想到 Flutter 中另一個非常重要的 widget,那就是 InheritedWidget。

如果你已經是一個 Flutter 小能手,或者已經看過《Flutter 開發之旅從南到北》和之前的文章的話,一定已經對他的作用有了清晰的認識了。

InheritedWidget 狀態可遺傳的屬性可以幫助我們很方便的實現父子元件之間的資料傳遞,同時,它也可以作為狀態管理中的 資料倉庫,作為整個應用的資料狀態統一儲存的地方。

圖源《Flutter 開發之旅從南到北》—— 第九章圖 9.4

上面程式碼中,我們透過繼承 InheritedWidget 就實現了自己的可遺傳元件 _InheritedStateContainer,其中的 data 變數表示全域狀態資料,在這裡就可以被認為是整個應用的一個單例物件

_InheritedStateContainer 還接受 child 引數作為它的子元件,child 表示的所以子元件們就都能夠以某種方式得到 data 這個單一的全域資料了。

約定俗成地,Flutter 原始碼經常會提供一些 of 方法(類比 getInstance())作為幫助我們拿到全域資料的輔助函式。

以 Flutter 中典型的 Theme 物件為例。我們通常會在應用的根元件 MaterialApp 中建立 ThemeData 物件作為應用統一的主題樣式物件:

MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
    visualDensity: VisualDensity.adaptivePlatformDensity,
  ),
  home: MyHomePage(title: 'Flutter Demo Home Page'),
);

在其他任意的元件中,我們可以使用 Theme.of(context) 拿到該物件了,且這個物件全域唯一。如下所示,我們可以將該 ThemeData 物件中的 primaryColor 應用在 Text 中:

// 使用全域文字樣式
Text(
  'Flutter',
  style: TextStyle(color: Theme.of(context).primaryColor),
)

這個角度來看,InheritedWidget 完全可以被我們看作是最原生、最 Flutter 的單例應用了。

本文小結

本篇文章,我們經歷了從實現普通單例到應用 getter 運算子 的 Dart 單例,到使用 工廠建構式函式 Dart 單例,再到使用了 工廠建構式函式 + 空安全語法 + 箭頭函式 的 Dart 單例,最後結合對 InheritedWidget 概念的理解,看到了 Flutter 中特有的單例模式,算是每一步都走了一遍。但學習設計模式的重點還是在於實際應用,希望大家今後在實際工程中能將這些概念用起來,如果你想更進一步理解 Dart 中的單例模式,可以參閱「拓展閱讀」學習更多,希望對你有幫助。

單例模式從南到北

拓展閱讀

關於本系列文章

Flutter / Dart 設計模式從南到北(簡稱 Flutter 設計模式)系列內容預計兩週發布一篇,著重向開發者介紹 Flutter 應用開發中常見的設計模式以及開發方式,旨在推進 Flutter / Dart 語言屬性的普及,以及幫助開發者更高效地開發出高質量、可維護的 Flutter 應用。