原文作者:Bezier
原文鏈接:https://juejin.cn/user/2629687546479742
前言
關於Android架構,可能在很多人心裏一直都是虛無縹緲的存在,似懂非懂、爲了用而用、處處生搬硬套,這種情況使用的意義真的很有限。本人有多個項目重構的經驗,恰好對設計領域較爲感興趣,今天我將毫無保留的將自己對架構、設計的理解分享給大家。
本文不會具體去講什麼是MVC、MVP、MVVM
,但我描述的點應該都是這些模式的基石
,從本質上講明白爲什麼
這樣做,這樣做的好處
是什麼,有了這些底層思想
的支持再去看對應的架構模式
,相信會讓你有一種煥然一新的感覺。
知識儲備:需掌握Java面向對象、六大設計原則
,如果不理解也無妨,我儘量將用到的設計原則加以詳細描述
目錄
-
1. 模塊化的意義何在? -
1.1 基本概念以及底層思想 -
1.2 我們要基於哪些特性去做模塊化劃分? -
1.3 Android如何做分層處理? -
1.4 Data Mapper或許是解藥 -
1.5 無處安放的業務邏輯 -
2. 合理分層是給 數據驅動UI 做鋪墊 -
2.1 什麼是 控制反轉? -
2.2 什麼是數據驅動UI? -
2.3 爲什麼說數據驅動UI底層思想是控制反轉? -
2.4 爲什麼引入Diff? -
3. 爲什麼我建議使用 函數式編程 -
3.1 什麼是 函數式編程? -
3.2 Android視圖開發可以借鑑函數式編程思想
1. 模塊化的意義何在?
1.1 基本概念以及底層思想
所有的模塊化都是爲了滿足單一設計原則 (字面意思理解即可),一個函數或者一個類再或者一個模塊,職責越單一複用性就越強,同時能夠間接降低耦合性
在軟件工程的背景下,改動就會有出錯的可能,不要說"我注意一點就不會出錯"
這種話,因爲人不是機器。我們能做的就是儘可能讓模塊更加單一,職責越單一影響到外層模塊的可能性就越小,這樣出錯的概率也就越低。
所以模塊化核心思想即:單一設計原則
1.2 我們要基於哪些特性去做模塊化劃分?
做模塊化處理的時候儘量基於兩種特性進行功能特性
、業務特性
功能特性
網絡、圖片加載等等都可稱之爲功能特性。比如網絡:我們可以將網絡框架的集成、封裝等等寫到同一個
模塊(module、package等)
當中,這樣可以增強可讀性(同一目錄一目瞭然)
、降低誤操作
概率,方便於維護也更加安全。同時也可將模塊託管至遠程如maven庫,可供多個項目使用,進一步提升複用性
業務特性
業務特性字面意思理解即可,就是我們常常編寫的業務,需要以業務的特性進行模塊劃分
爲什麼說業務特性
優先級要高於功能特性
?
舉個例子如下圖:
相信很多人見過或者正在使用這種分包方式,在業務層把所有的Adapter
、Presenter
、Activity
等等都放在對應的包中,這種方式合理嗎?先說答案不合理
,首先這已經是在業務層,我們做的所有事情其實都在爲業務層服務,所以業務的優先級應該是最高的,我們應當優先根據業務特性將對應的類放入到同一個包中。
功能模塊
核心是功能,應當以功能
進行模塊劃分。業務模塊
核心是業務,應當優先以業務
進行模塊劃分,其次再以功能
進行模塊劃分。
1.3 Android如何做分層處理?
前端開發其實就是做數據搬運,再展示到視圖中。數據
與視圖
是兩個不同的概念,爲了提高複用性以及可維護性,我們應當根據單一設計原則
我們應當將二者進行分層處理,所以無論是MVC
、MVP
還是MVVM
最核心的點都是將數據
與視圖
進行分層。
絆腳石:
通常來講,我們通過網絡請求拿到數據結構都是後端定義的,這也就意味着視圖層不得不直接使用後端定義的字段,一旦後端進行業務調整會迫使我們前端從
數據層-->視圖層
都會進行對應的改動,如下僞代碼所示:
//原始邏輯
數據層
Model{
title
}
UI層
View{
textView = model.title
}
//後端調整後
數據層
Model{
title
prefix
}
UI層
View{
textView = model.prefix + model.title
}
起初我們的textView
顯示的是model
中的title
,但後端調整後我們需要在model
中加一個prefix
字段,同時textView
顯示內容也要做一次字符串拼接。視圖層因爲數據層的改動而被動做了修改。既然做了分層我們想要的肯定是視圖、數據互不干擾,如何解決?往下看...
1.4 Data Mapper或許是解藥
Data Mapper
是後端常用的一個概念,一般情況下他們是不會直接使用數據庫裏面的字段,而是加一個Data Mapper(數據映射)
將數據庫錶轉按需換成Java Bean
,這樣做的好處也很明顯,表結構甭管怎麼折騰都不會影響到業務層代碼。
對於前端我覺得可以適當引入Data Mapper
,將後端數據轉換成本地模型
,本地模型只與設計圖對應,將後端業務
與視圖
完全隔離。這也就解決了 1.3 面臨的問題,具體方式如下:
數據層
Model{
title
prefix
}
本地模型(與設計圖一一對應)
LocalModel{
//將後端模型轉換爲本地模型
title = model.prefix + model.title
}
UI層
View{
textView = localModel.title
}
LocalModel
相當於一箇中間層,通過適配器模式
將數據層與視圖層做隔離。
前端引入Data Mapper
後可以脫離後端進行開發,只要需求明確就可以做視圖
層的開發,完全不需要擔心後端返回什麼結構
、字段
。並且這種做法是一勞永逸的,比如後端需要對某些字段做調整,我們可以不暇思索直奔數據層
,涉及到的調整100%不會影響到視圖層
注意點:
當下有一部分公司爲了將前後端分離更徹底,由前端開發人員提供
Java Bean(相當於LocalModel)
的結構,好處也很明顯,更多的業務內聚到後端,很大程度提升了業務的靈活性,畢竟App發一次版成本還是比較大的。面對這種情況我們其實沒必要再編寫Data Mapper
。所以任何架構設計都要結合實際情況,適合自己的纔是最好的。
1.5 無處安放的業務邏輯
關於業務邏輯
其實是一個很籠統的概念,甚至可以將任意一行代碼稱之爲業務邏輯
,如此寬泛的概念我們該如何去理解?我先大致將它分爲兩個方面:
界面交互邏輯:視圖層的交互邏輯,比如手勢控制、吸頂懸浮等等都是根據業務需要實現的,所以嚴格來說這部分也屬於業務邏輯。但這部分 業務邏輯
一般在視圖層實現。數據邏輯:這部分是大家常說的業務邏輯,屬於強業務邏輯,比如根據不同用戶類型獲取不同數據、展示不同界面,加上Data Mapper一系列操作其實就是給後端兜底,幫他們補全剩餘邏輯而已。爲了方便大家理解下文我將 數據邏輯
統稱爲業務邏輯
。
前面我們說到,Android開發應該具備數據層
跟視圖層
,那業務邏輯放在哪一層比較合適呢?比如MVVM
模式下大家都說將業務邏輯
放到ViewModel
處理,這麼說也沒有太大的問題,但如果一個界面足夠複雜那對應的ViewModel
代碼可能會有成百上千行,看起來會很臃腫可讀性也非常差。最重要的一點這些業務很難編寫單元測試用例
。
關於業務邏輯我建議單獨寫一個use case
處理。
use case
通常放在ViewModeler
與數據層
之間,業務邏輯以及Data Mapper
都應該放在use case
中,每一個行爲對應一個use case
。這樣就解決了ViewModeler
臃腫的問題,同時更方便編寫測試用例。
注意點:
好的設計都是特定場景解決特定問題,過度設計不僅解決不了任何問題反而會增加開發成本。以我目前經驗來看Android開發至少一半的場景都很簡單:
請求-->拿數據-->渲染視圖
最多再加個Data Mapper
,流程很單一併且後期改動的可能也不太大,這種情況就沒必要寫一個use case,Data Mapper
扔到數據層即可。
2. 合理分層是給 數據驅動UI 做鋪墊
先說結論:數據驅動UI的本質是控制反轉
2.1 什麼是 控制反轉?
控制
即對程序流程的控制,一般由我們開發者承擔,此過程爲控制
。但開發者是人所以不可避免出現錯誤,此時可以將角色做一個反轉
由成熟的框架負責整個流程,程序員只需要在框架預留的擴展點上,添加跟自己的業務代碼,就可以利用框架來驅動整個程序流程的執行,此過程爲反轉
。
控制反轉
概念和設計原則中的依賴倒置
很相似,只是少了一個依賴抽象
。
打個比方:
現有一個
HTTP請求
的需求,如果想自己維護HTTT鏈接
、自己管理TCP Socket
、自己處理HTTP緩存
.....就是整個HTTP協議
全部自己封裝,先不說這個工程能不能靠個人實現,就算實現也是漏洞百出,此時可以換個思路:通過OkHttp
去實現,OkHttp
是一個成熟的框架用它基本上不會出錯。個人封裝HTTP協議
到使用OkHttp框架
,這個過程在控制
HTTP的角色上發生了一個反轉
,個人--->成熟的框架OkHttp
即控制反轉,好處也很明顯,框架出錯的概率遠低於個人。
2.2 什麼是數據驅動UI?
通俗一點說就是當數據改變時對應的UI也要跟着變,反過來說當需要改變UI只需要改變對應的數據即可。現在比較流行的UI框架如Flutter
、Compose
、Vue
其本質都是基於函數式編程實現數據驅動UI,它們共同的目的都是爲了解決數據,UI一致性問題。
在當前的Android中可以使用DataBinding
實現同樣的效果,以Jetpack MVVM
爲例:ViewModel
從Repository
拿到數據暫存到ViewModel
對應的ObservableFiled
即可實現數據驅動UI,但前提是從Repository
拿到的數據可以直接用,如果在Activity
或者Adapter
做數據二次處理再notify UI
,已經違背數據驅動UI核心思想。所以想實現數據驅動UI必須要有合理的分層(UI層拿到的數據無需處理,可以直接用)
,Data Mapper
恰好解決這一問題,同時也可規避大量編寫BindAdapter
的現狀。
DataBinding
並非函數式編程,它只是通過AbstractProcessor
生成中間代碼,將數據映射到XML中
2.3 爲什麼說數據驅動UI底層思想是控制反轉?
當前Android生態能實現數據綁定UI的框架只有兩個:DataBinding、Compose(暫不討論)
在引入DataBinding之前渲染一條數據通常需要兩步,如下:
var title = "iOS"
fun setTitle(){
//第一步更改數據源
title = "Android"
//第二個更改UI
textView = title
}
共需要兩步更改數據源、更改UI,數據源
跟UI
有一個忘記修改便會出現BUG,千萬不要說:“兩個我都不會忘記修改
”,當面臨複雜的邏輯以及十幾個甚至幾十個的數據源很難保證不出錯。這種問題可以通過DataBinding
解決,只需更改對應的ObservableFiled
UI便會同步修改,控制
UI狀態也從個人反轉
到的DataBinding
,個人疏忽的事情DataBinding
可不會。
所以說數據驅動UI底層思想是控制反轉
2.4 爲什麼引入Diff?
引入diff
之前:
RecyclerView
想要實現動態刪除、添加、更新需要分別手動更新數據和UI,這樣在中間插了一道
並且分別更新數據和UI已經違背了前面所說的數據驅動UI
,而我們想要的是不管刪除、添加或者更新只有一個入口,只要改變數據源就會驅動UI做更新,想要滿足這一原則只能改變數據源後對RecyclerView
做全部刷新,但這樣會造成性能問題,複雜的界面會感到明顯的卡頓。
引入diff
之後:
Diff
算法通過對oldItem
和newItem
做差異化比對,會自動更新改變的item
,同時支持刪除、添加的動畫效果,這一特性解決了RecyclerView
需要實現數據驅動UI
的性能問題
3 爲什麼我建議使用 函數式編程
3.1 什麼是 函數式編程?
-
一個入口,一個出口。 -
不在函數鏈內部執行與運算本身無關的操作 -
不在函數鏈內部使用外部變量(實際上這一條很難遵守,可以適當突破)
說的通俗點就是給定一個初始值,經過函數鏈的運行會得到一個目標值,運算的過程中外部沒有插手的權限,同時不做與本身無關的操作,從根本上解決了不可預期錯誤的產生。
舉個例子:
//Kotlin代碼
listOf(10, 20).map {
it + 1
}.forEach {
Log.i("list", "$it")
}
上面這種鏈式編程就是標準的函數式編程,輸入到輸出之間開發者根本沒有插手的機會(即Log.i(..)
之前開發者沒有權限處理list),所以整個流程是100%
安全的,RxJava
、Flow
、鏈式高階函數
都是標準的函數式編程,它們從規範
層面解決數據安全問題。所以我建議在Kotlin
中 碰到數據處理儘量使用鏈式高階函數(RxJava、Kotlin Flow亦然)
。
3.2 Android視圖開發可以借鑑函數式編程思想
Android視圖開發大都遵循如下流程:請求-->處理數據-->渲染UI,這一流程可以借鑑函數式編程,將請求作爲入口,渲染做爲出口,在這個流程中儘量不做與當前行爲無關的事(這也要求ViewModel
,Repository
中的函數要符合單一原則)。這樣說有點籠統,下面舉個反例:
View{
//刷新
fun refresh(){
ViewModel.load(true)
}
//加載更多
fun loadMore(){
ViewModel.load(false)
}
}
ViewModel{
//加載數據
load(isRefresh){
if (isRefresh){
//刷新
}else{
//加載更多
}
}
}
View
層有刷新、加載更多兩種行爲,load(isRefresh)
一個入口,兩個出口。面臨的問題很明顯,修改刷新
或加載更多
都會對對方產生影響,違反開閉原則
中的閉(對修改關閉:行爲沒變不準修改源代碼)
,導致存在不可預期的問題產生。可以借鑑函數式編程
思想對其進行改進,將ViewModel
的load
函數拆分成refresh
和loadMore
,這樣刷新
和加載更多
兩種行爲、兩個入口、兩個出口互不干涉,通過函數的銜接形成兩條獨立的業務鏈條。
函數式編程可以約束我們寫出規範的代碼,面對不能使用函數式編程的場景,我們可以嘗試自我約束往函數式編程方向靠攏,大致也能實現相同的效果。
綜上所述
-
合理的分層可以提升複用性、降低模塊間耦合性 -
Data Mapper 可以讓視圖層脫離於後端進行開發 -
複雜的業務邏輯應該寫到use case中 -
數據驅動UI的本質是控制反轉 -
通過函數式編程可以寫出更加安全的代碼
如果大家對Jetpack MVVM
感興趣歡迎留言,下篇文章我可以寫一下自己的看法..
參考文章:KunMinX 之 MVVM系列
本文分享自微信公衆號 - 秉心說TM(gh_c6504b1af5ae)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。