今天,我們來聊一聊 Flutter 應用的工程架構這個話題。
在軟件開發中,我們不僅要在代碼實現中遵守常見的設計模式,更需要在架構設計中遵從基本的設計原則。而在這其中,DRY(即 Don’t Repeat Yourself)原則可以算是最重要的一個。
通俗來講,DRY 原則就是“不要重復”。這是一個很樸素的概念,因為即使是最初級的開發者,在寫了一段時間代碼后,也會不自覺地把一些常用的重復代碼抽取出來,放到公用的函數、類或是獨立的組件庫中,從而實現代碼復用。
在軟件開發中,我們通常從架構設計中就要考慮如何去管理重復性(即代碼復用),即如何將功能進行分治,將大問題分解為多個較為獨立的小問題。而在這其中,組件化和平臺化就是客戶端開發中最流行的分治手段。
所以今天,我們就一起來學習一下這兩類分治復用方案的中心思想,這樣我們在設計 Flutter 應用的架構時也就能做到有章可循了。
## 組件化
組件化又叫模塊化,即基于可重用的目的,將一個大型軟件系統(App)按照關注點分離的方式,拆分成多個獨立的組件或模塊。每個獨立的組件都是一個單獨的系統,可以單獨維護、升級甚至直接替換,也可以依賴于別的獨立組件,只要組件提供的功能不發生變化,就不會影響其他組件和軟件系統的整體功能。
:-: 
圖 1 組件化示意圖
可以看到,組件化的中心思想是將獨立的功能進行拆分,而在拆分粒度上,組件化的約束則較為松散。一個獨立的組件可以是一個軟件包(Package)、頁面、UI 控件,甚至可能是封裝了一些函數的模塊。
**組件的粒度可大可小,那我們如何才能做好組件的封裝重用呢?哪些代碼應該被放到一個組件中?**這里有一些基本原則,包括單一性原則、抽象化原則、穩定性原則和自完備性原則。
接下來,我們先看看這些原則具體是什么意思。
**單一性原則指的是**,每個組件僅提供一個功能。分而治之是組件化的中心思想,每個組件都有自己固定的職責和清晰的邊界,專注地做一件事兒,這樣這個組件才能良性發展。
一個反例是 Common 或 Util 組件,這類組件往往是因為在開發中出現了定義不明確、歸屬邊界不清晰的代碼:“哎呀,這段代碼放哪兒好像都不合適,那就放 Common(Util)吧”。久而久之,這類組件就變成了無人問津的垃圾堆。所以,再遇到不知道該放哪兒的代碼時,就需要重新思考組件的設計和職責了。
**抽象化原則**指的是,組件提供的功能抽象應該盡量穩定,具有高復用度。而穩定的直觀表現就是對外暴露的接口很少發生變化,要做到這一點,需要我們提升對功能的抽象總結能力,在組件封裝時做好功能抽象和接口設計,將所有可能發生變化的因子都在組件內部做好適配,不要暴露給它的調用方。
**穩定性原則**指的是,不要讓穩定的組件依賴不穩定的組件。比如組件 1 依賴了組件 5,如果組件 1 很穩定,但是組件 5 經常變化,那么組件 1 也就會變得不穩定了,需要經常適配。如果組件 5 里確實有組件 1 不可或缺的代碼,我們可以考慮把這段代碼拆出來單獨做成一個新的組件 X,或是直接在組件 1 中拷貝一份依賴的代碼。
**自完備性**,即組件需要盡可能地做到自給自足,盡量減少對其他底層組件的依賴,達到代碼可復用的目的。比如,組件 1 只是依賴某個大組件 5 中的某個方法,這時更好的處理方法是,剝離掉組件 1 對組件 5 的依賴,直接把這個方法拷貝到組件 1 中。這樣一來組件 1 就能夠更好地應對后續的外部變更了。
在理解了組件化的基本原則之后,**我們再來看看組件化的具體實施步驟**,即剝離基礎功能、抽象業務模塊和最小化服務能力。
首先,我們需要剝離應用中與業務無關的基礎功能,比如網絡請求、組件中間件、第三方庫封裝、UI 組件等,將它們封裝為獨立的基礎庫;然后,我們在項目里用 pub 進行管理。如果是第三方庫,考慮到后續的維護適配成本,我們最好再封裝一層,使項目不直接依賴外部代碼,方便后續更新或替換。
基礎功能已經封裝成了定義更清晰的組件,接下來我們就可以按照業務維度,比如首頁、詳情頁、搜索頁等,去拆分獨立的模塊了。拆分的粒度可以先粗后細,只要能將大體劃分清晰的業務組件進行拆分,后續就可以通過分布迭代、局部微調,最終實現整個業務項目的組件化。
在業務組件和基礎組件都完成拆分封裝后,應用的組件化架構就基本成型了,最后就可以按照剛才我們說的 4 個原則,去修正各個組件向下的依賴,以及最小化對外暴露的能力了。
## 平臺化
從組件的定義可以看到,組件是個松散的廣義概念,其規模取決于我們封裝的功能維度大小,而各個組件之間的關系也僅靠依賴去維持。如果組件之間的依賴關系比較復雜,就會在一定程度上造成功能耦合現象。
如下所示的組件示意圖中,組件 2 和組件 3 同時被多個業務組件和基礎功能組件直接引用,甚至組件 2 和組件 5、組件 3 和組件 4 之間還存在著循環依賴的情況。一旦這些組件的內部實現和外部接口發生變化,整個 App 就會陷入不穩定的狀態,即所謂牽一發而動全身。
:-: 
圖 2 循環依賴現象
平臺化是組件化的升級,即在組件化的基礎上,對它們提供的功能進行分類,統一分層劃分,增加依賴治理的概念。為了對這些功能單元在概念上進行更為統一的分類,我們按照四象限分析法,把應用程序的組件按照業務和 UI 分解為 4 個維度,來分析組件可以分為哪幾類。
:-: 
圖 3 組件劃分原則
可以看出,經過業務與 UI 的分解之后,這些組件可以分為 4 類:
1. 具備 UI 屬性的獨立業務模塊;
2. 不具備 UI 屬性的基礎業務功能;
3. 不具備業務屬性的 UI 控件
4. 不具備業務屬性的基礎功能
按照自身定義,這 4 類組件其實隱含著分層依賴的關系。比如,處于業務模塊中的首頁,依賴位于基礎業務模塊中的賬號功能;再比如,位于 UI 控件模塊中的輪播卡片,依賴位于基礎功能模塊中的存儲管理等功能。我們將它們按照依賴的先后順序從上到下進行劃分,就是一個完整的 App 了。
:-: 
圖 4 組件化分層
可以看到,平臺化與組件化最大的差異在于增加了分層的概念,每一層的功能均基于同層和下層的功能之上,這使得各個組件之間既保持了獨立性,同時也具有一定的彈性,在不越界的情況下按照功能劃分各司其職。
**與組件化更關注組件的獨立性相比,平臺化更關注的是組件之間關系的合理性,而這也是在設計平臺化架構時需要重點考慮的單向依賴原則。**
所謂單向依賴原則,指的是組件依賴的順序應該按照應用架構的層數從上到下依賴,不要出現下層模塊依賴上層模塊這樣循環依賴的現象。這樣可以最大限度地避免復雜的耦合,減少組件化時的困難。如果我們每個組件都只是單向依賴其他組件,各個組件之間的關系都是清晰的,代碼解耦也就會變得非常輕松了。
平臺化強調依賴的順序性,除了不允許出現下層組件依賴上層組件的情況,跨層組件和同層組件之間的依賴關系也應當嚴格控制,因為這樣的依賴關系往往會帶來架構設計上的混亂。
**如果下層組件確實需要調用上層組件的代碼怎么辦?**
這時,我們可以采用增加中間層的方式,比如 Event Bus、Provider 或 Router,以中間層轉發的形式實現信息同步。比如,位于第 4 層的網絡引擎中,會針對特定的錯誤碼跳轉到位于第 1 層的統一錯誤頁,這時我們就可以利用 Router 提供的命名路由跳轉,在不感知錯誤頁的實現情況下來完成。又比如,位于第 2 層的賬號組件中,會在用戶登入登出時主動刷新位于第 1 層的首頁和我的頁面,這時我們就可以利用 Event Bus 來觸發賬號切換事件,在不需要獲取頁面實例的情況下通知它們更新界面。關于這部分內容,你可以參考第[20](https://time.geekbang.org/column/article/116382)和[21](https://time.geekbang.org/column/article/118421)篇文章中的相關內容,這里就不再贅述了。
**平臺化架構是目前應用最廣的軟件架構設計,其核心在于如何將離散的組件依照單向依賴的原則進行分層。**而關于具體的分層邏輯,除了我們上面介紹的業務和 UI 四象限法則之外,你也可以使用其他的劃分策略,只要整體結構層次清晰明確,不存在難以確定歸屬的組件就可以了。
比如,Flutter 就采用 Embedder(操作系統適配層)、Engine(渲染引擎及 Dart VM 層)和 Framework(UI SDK 層)整體三層的劃分。可以看到,Flutter 框架每一層的組件定義都有著明確的邊界,其向上提供的功能和向下依賴的能力也非常明確。
:-: 
圖 5 Flutter 框架架構
備注:此圖引自[Flutter System Overview](https://flutter.dev/docs/resources/technical-overview)
## 總結
好了,今天的分享就到這里,我們總結一下主要內容吧。
組件化和平臺化都是軟件開發中流行的分治手段,能夠將 App 內的功能拆分成多個獨立的組件或模塊。
其中,組件化更關注如何保持組件的獨立性,只要拆分的功能獨立即可,約束較為松散,在中大型 App 中容易造成一定程度的功能耦合現象。而平臺化則更強調組件之間關系的合理性,增加了分層的概念,使得組件之間既有邊界,也有一定的彈性。只要滿足單向依賴原則,各個組件之間的關系都是清晰的。
分治是一種與技術無關的架構思想,有利于降低工程的復雜性,從而提高 App 的可擴展和可維護性。今天這篇文章,我重點與你分享的是組件化與平臺化這兩種架構設計的思路,并沒有講解它們的具體實現。而關于組件化與平臺化的實現細節,網絡上已經有很多文章了,你可以在網上自行搜索了解。如果你還有關于組件化和平臺化的其他問題,那就在評論區中給我留言吧。
其實,你也可以琢磨出,今天這篇文章的目的是帶你領會 App 架構設計的核心思想。因為,理解思想之后剩下的就是去實踐了,當你需要設計 App 架構時再回憶起這些內容,或是翻出這篇文章一定會事半功倍。
## 思考題
最后,我給你留一道思考題吧。
在 App 架構設計中,你會采用何種方式去管理涉及資源類的依賴呢?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略