文/ 楊加康,CFUG 社區成員,《Flutter 開發之旅從南到北》作者,小米工程師
單例設計模式(Singleton Design Pattern)理解起來非常簡單。
一個類只允許創建一個實例,那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。
作為最簡單的一種設計模式之一,對於單例本身的概念,大家一看就能明白,但在某些情況下也很容易使用不恰當。相比其他語言,Dart 和 Flutter 中的單例模式也不盡相同,本篇文章我們就一起探究看看它在 Dart 和 Flutter 中的應用。
Flutter(able) 的單例模式
一般來說,要在代碼中使用單例模式,結構上會有下面這些約定俗成的要求:
- 單例類(Singleton)中包含一個引用自身類的靜態屬性實例(instance),且能自行創建這個實例。
- 該實例只能通過靜態方法
getInstance()
訪問。 - 類構造函數通常沒有參數,且被標記為私有,確保不能從類外部實例化該類。
遵循以上這些要求,我們就不難能用 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 狀態可遺傳的特性可以幫助我們很方便的實現父子組件之間的數據傳遞,同時,它也可以作為狀態管理中的 數據倉庫,作為整個應用的數據狀態統一保存的地方。
上面代碼中,我們通過繼承 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 開發之旅從南到北》—— 第 2 章、第 9 章
- 單例模式
- Dart 空安全
- 延遲初始化
關於本系列文章
Flutter / Dart 設計模式從南到北(簡稱 Flutter 設計模式)系列內容預計兩周發布一篇,着重向開發者介紹 Flutter 應用開發中常見的設計模式以及開發方式,旨在推進 Flutter / Dart 語言特性的普及,以及幫助開發者更高效地開發出高質量、可維護的 Flutter 應用。