關(guān)于Android架構(gòu),你是否還在生搬硬套?
前言
關(guān)于Android架構(gòu),可能在很多人心里一直都是虛無(wú)縹緲的存在,似懂非懂、為了用而用、處處生搬硬套,這種情況使用的意義真的很有限。本人有多個(gè)項(xiàng)目重構(gòu)的經(jīng)驗(yàn),恰好對(duì)設(shè)計(jì)領(lǐng)域較為感興趣,今天我將毫無(wú)保留的將自己對(duì)架構(gòu)、設(shè)計(jì)的理解分享給大家。
本文不會(huì)具體去講什么是MVC、MVP、MVVM,但我描述的點(diǎn)應(yīng)該都是這些模式的基石,從本質(zhì)上講明白為什么這樣做,這樣做的好處是什么,有了這些底層思想的支持再去看對(duì)應(yīng)的架構(gòu)模式,相信會(huì)讓你有一種煥然一新的感覺(jué)。
知識(shí)儲(chǔ)備:需掌握Java面向?qū)ο?、六大設(shè)計(jì)原則,如果不理解也無(wú)妨,我盡量將用到的設(shè)計(jì)原則加以詳細(xì)描述
目錄
1. 模塊化的意義何在? 1.1 基本概念以及底層思想 1.2 我們要基于哪些特性去做模塊化劃分? 1.3 Android如何做分層處理? 1.4 Data Mapper或許是解藥 1.5 無(wú)處安放的業(yè)務(wù)邏輯 2. 合理分層是給 數(shù)據(jù)驅(qū)動(dòng)UI 做鋪墊 2.1 什么是 控制反轉(zhuǎn)? 2.2 什么是數(shù)據(jù)驅(qū)動(dòng)UI? 2.3 為什么說(shuō)數(shù)據(jù)驅(qū)動(dòng)UI底層思想是控制反轉(zhuǎn)? 2.4 為什么引入Diff? 3. 為什么我建議使用 函數(shù)式編程 3.1 什么是 函數(shù)式編程? 3.2 Android視圖開(kāi)發(fā)可以借鑒函數(shù)式編程思想
1. 模塊化的意義何在?
1.1 基本概念以及底層思想
所有的模塊化都是為了滿(mǎn)足單一設(shè)計(jì)原則 (字面意思理解即可),一個(gè)函數(shù)或者一個(gè)類(lèi)再或者一個(gè)模塊,職責(zé)越單一復(fù)用性就越強(qiáng),同時(shí)能夠間接降低耦合性
在軟件工程的背景下,改動(dòng)就會(huì)有出錯(cuò)的可能,不要說(shuō)"我注意一點(diǎn)就不會(huì)出錯(cuò)"這種話(huà),因?yàn)槿瞬皇菣C(jī)器。我們能做的就是盡可能讓模塊更加單一,職責(zé)越單一影響到外層模塊的可能性就越小,這樣出錯(cuò)的概率也就越低。
所以模塊化核心思想即:單一設(shè)計(jì)原則
1.2 我們要基于哪些特性去做模塊化劃分?
做模塊化處理的時(shí)候盡量基于兩種特性進(jìn)行功能特性、業(yè)務(wù)特性
功能特性
網(wǎng)絡(luò)、圖片加載等等都可稱(chēng)之為功能特性。比如網(wǎng)絡(luò):我們可以將網(wǎng)絡(luò)框架的集成、封裝等等寫(xiě)到同一個(gè)
模塊(module、package等)當(dāng)中,這樣可以增強(qiáng)可讀性(同一目錄一目了然)、降低誤操作概率,方便于維護(hù)也更加安全。同時(shí)也可將模塊托管至遠(yuǎn)程如maven庫(kù),可供多個(gè)項(xiàng)目使用,進(jìn)一步提升復(fù)用性
業(yè)務(wù)特性
業(yè)務(wù)特性字面意思理解即可,就是我們常常編寫(xiě)的業(yè)務(wù),需要以業(yè)務(wù)的特性進(jìn)行模塊劃分
為什么說(shuō)業(yè)務(wù)特性優(yōu)先級(jí)要高于功能特性?
舉個(gè)例子如下圖:

相信很多人見(jiàn)過(guò)或者正在使用這種分包方式,在業(yè)務(wù)層把所有的Adapter、Presenter、Activity等等都放在對(duì)應(yīng)的包中,這種方式合理嗎?先說(shuō)答案不合理,首先這已經(jīng)是在業(yè)務(wù)層,我們做的所有事情其實(shí)都在為業(yè)務(wù)層服務(wù),所以業(yè)務(wù)的優(yōu)先級(jí)應(yīng)該是最高的,我們應(yīng)當(dāng)優(yōu)先根據(jù)業(yè)務(wù)特性將對(duì)應(yīng)的類(lèi)放入到同一個(gè)包中。
功能模塊核心是功能,應(yīng)當(dāng)以功能進(jìn)行模塊劃分。業(yè)務(wù)模塊核心是業(yè)務(wù),應(yīng)當(dāng)優(yōu)先以業(yè)務(wù)進(jìn)行模塊劃分,其次再以功能進(jìn)行模塊劃分。
1.3 Android如何做分層處理?
前端開(kāi)發(fā)其實(shí)就是做數(shù)據(jù)搬運(yùn),再展示到視圖中。數(shù)據(jù)與視圖是兩個(gè)不同的概念,為了提高復(fù)用性以及可維護(hù)性,我們應(yīng)當(dāng)根據(jù)單一設(shè)計(jì)原則我們應(yīng)當(dāng)將二者進(jìn)行分層處理,所以無(wú)論是MVC、MVP還是MVVM最核心的點(diǎn)都是將數(shù)據(jù)與視圖進(jìn)行分層。
絆腳石:
通常來(lái)講,我們通過(guò)網(wǎng)絡(luò)請(qǐng)求拿到數(shù)據(jù)結(jié)構(gòu)都是后端定義的,這也就意味著視圖層不得不直接使用后端定義的字段,一旦后端進(jìn)行業(yè)務(wù)調(diào)整會(huì)迫使我們前端從
數(shù)據(jù)層-->視圖層都會(huì)進(jìn)行對(duì)應(yīng)的改動(dòng),如下偽代碼所示:
//原始邏輯
數(shù)據(jù)層
Model{
title
}
UI層
View{
textView = model.title
}
//后端調(diào)整后
數(shù)據(jù)層
Model{
title
prefix
}
UI層
View{
textView = model.prefix + model.title
}
起初我們的textView顯示的是model中的title,但后端調(diào)整后我們需要在model中加一個(gè)prefix字段,同時(shí)textView顯示內(nèi)容也要做一次字符串拼接。視圖層因?yàn)閿?shù)據(jù)層的改動(dòng)而被動(dòng)做了修改。既然做了分層我們想要的肯定是視圖、數(shù)據(jù)互不干擾,如何解決?往下看...
1.4 Data Mapper或許是解藥
Data Mapper是后端常用的一個(gè)概念,一般情況下他們是不會(huì)直接使用數(shù)據(jù)庫(kù)里面的字段,而是加一個(gè)Data Mapper(數(shù)據(jù)映射)將數(shù)據(jù)庫(kù)表轉(zhuǎn)按需換成Java Bean,這樣做的好處也很明顯,表結(jié)構(gòu)甭管怎么折騰都不會(huì)影響到業(yè)務(wù)層代碼。
對(duì)于前端我覺(jué)得可以適當(dāng)引入Data Mapper,將后端數(shù)據(jù)轉(zhuǎn)換成本地模型,本地模型只與設(shè)計(jì)圖對(duì)應(yīng),將后端業(yè)務(wù)與視圖完全隔離。這也就解決了 1.3 面臨的問(wèn)題,具體方式如下:
數(shù)據(jù)層
Model{
title
prefix
}
本地模型(與設(shè)計(jì)圖一一對(duì)應(yīng))
LocalModel{
//將后端模型轉(zhuǎn)換為本地模型
title = model.prefix + model.title
}
UI層
View{
textView = localModel.title
}
LocalModel相當(dāng)于一個(gè)中間層,通過(guò)適配器模式將數(shù)據(jù)層與視圖層做隔離。
前端引入Data Mapper后可以脫離后端進(jìn)行開(kāi)發(fā),只要需求明確就可以做視圖層的開(kāi)發(fā),完全不需要擔(dān)心后端返回什么結(jié)構(gòu)、字段。并且這種做法是一勞永逸的,比如后端需要對(duì)某些字段做調(diào)整,我們可以不暇思索直奔數(shù)據(jù)層,涉及到的調(diào)整100%不會(huì)影響到視圖層
注意點(diǎn):
當(dāng)下有一部分公司為了將前后端分離更徹底,由前端開(kāi)發(fā)人員提供
Java Bean(相當(dāng)于LocalModel)的結(jié)構(gòu),好處也很明顯,更多的業(yè)務(wù)內(nèi)聚到后端,很大程度提升了業(yè)務(wù)的靈活性,畢竟App發(fā)一次版成本還是比較大的。面對(duì)這種情況我們其實(shí)沒(méi)必要再編寫(xiě)Data Mapper。所以任何架構(gòu)設(shè)計(jì)都要結(jié)合實(shí)際情況,適合自己的才是最好的。
1.5 無(wú)處安放的業(yè)務(wù)邏輯
關(guān)于業(yè)務(wù)邏輯其實(shí)是一個(gè)很籠統(tǒng)的概念,甚至可以將任意一行代碼稱(chēng)之為業(yè)務(wù)邏輯,如此寬泛的概念我們?cè)撊绾稳ダ斫??我先大致將它分為兩個(gè)方面:
界面交互邏輯:視圖層的交互邏輯,比如手勢(shì)控制、吸頂懸浮等等都是根據(jù)業(yè)務(wù)需要實(shí)現(xiàn)的,所以嚴(yán)格來(lái)說(shuō)這部分也屬于業(yè)務(wù)邏輯。但這部分 業(yè)務(wù)邏輯一般在視圖層實(shí)現(xiàn)。數(shù)據(jù)邏輯:這部分是大家常說(shuō)的業(yè)務(wù)邏輯,屬于強(qiáng)業(yè)務(wù)邏輯,比如根據(jù)不同用戶(hù)類(lèi)型獲取不同數(shù)據(jù)、展示不同界面,加上Data Mapper一系列操作其實(shí)就是給后端兜底,幫他們補(bǔ)全剩余邏輯而已。為了方便大家理解下文我將 數(shù)據(jù)邏輯統(tǒng)稱(chēng)為業(yè)務(wù)邏輯。
前面我們說(shuō)到,Android開(kāi)發(fā)應(yīng)該具備數(shù)據(jù)層跟視圖層,那業(yè)務(wù)邏輯放在哪一層比較合適呢?比如MVVM模式下大家都說(shuō)將業(yè)務(wù)邏輯放到ViewModel處理,這么說(shuō)也沒(méi)有太大的問(wèn)題,但如果一個(gè)界面足夠復(fù)雜那對(duì)應(yīng)的ViewModel代碼可能會(huì)有成百上千行,看起來(lái)會(huì)很臃腫可讀性也非常差。最重要的一點(diǎn)這些業(yè)務(wù)很難編寫(xiě)單元測(cè)試用例。
關(guān)于業(yè)務(wù)邏輯我建議單獨(dú)寫(xiě)一個(gè)use case處理。
use case通常放在ViewModel/Presenter與數(shù)據(jù)層之間,業(yè)務(wù)邏輯以及Data Mapper都應(yīng)該放在use case中,每一個(gè)行為對(duì)應(yīng)一個(gè)use case。這樣就解決了ViewModel/Presenter臃腫的問(wèn)題,同時(shí)更方便編寫(xiě)測(cè)試用例。
注意點(diǎn):
好的設(shè)計(jì)都是特定場(chǎng)景解決特定問(wèn)題,過(guò)度設(shè)計(jì)不僅解決不了任何問(wèn)題反而會(huì)增加開(kāi)發(fā)成本。以我目前經(jīng)驗(yàn)來(lái)看Android開(kāi)發(fā)至少一半的場(chǎng)景都很簡(jiǎn)單:
請(qǐng)求-->拿數(shù)據(jù)-->渲染視圖最多再加個(gè)Data Mapper,流程很單一并且后期改動(dòng)的可能也不太大,這種情況就沒(méi)必要寫(xiě)一個(gè)use case,Data Mapper扔到數(shù)據(jù)層即可。
2. 合理分層是給 數(shù)據(jù)驅(qū)動(dòng)UI 做鋪墊
先說(shuō)結(jié)論:數(shù)據(jù)驅(qū)動(dòng)UI的本質(zhì)是控制反轉(zhuǎn)
2.1 什么是 控制反轉(zhuǎn)?
控制即對(duì)程序流程的控制,一般由我們開(kāi)發(fā)者承擔(dān),此過(guò)程為控制。但開(kāi)發(fā)者是人所以不可避免出現(xiàn)錯(cuò)誤,此時(shí)可以將角色做一個(gè)反轉(zhuǎn)由成熟的框架負(fù)責(zé)整個(gè)流程,程序員只需要在框架預(yù)留的擴(kuò)展點(diǎn)上,添加跟自己的業(yè)務(wù)代碼,就可以利用框架來(lái)驅(qū)動(dòng)整個(gè)程序流程的執(zhí)行,此過(guò)程為反轉(zhuǎn)。
控制反轉(zhuǎn)概念和設(shè)計(jì)原則中的依賴(lài)倒置很相似,只是少了一個(gè)依賴(lài)抽象。
打個(gè)比方:
現(xiàn)有一個(gè)
HTTP請(qǐng)求的需求,如果想自己維護(hù)HTTT鏈接、自己管理TCP Socket、自己處理HTTP緩存.....就是整個(gè)HTTP協(xié)議全部自己封裝,先不說(shuō)這個(gè)工程能不能靠個(gè)人實(shí)現(xiàn),就算實(shí)現(xiàn)也是漏洞百出,此時(shí)可以換個(gè)思路:通過(guò)OkHttp去實(shí)現(xiàn),OkHttp是一個(gè)成熟的框架用它基本上不會(huì)出錯(cuò)。個(gè)人封裝HTTP協(xié)議到使用OkHttp框架,這個(gè)過(guò)程在控制HTTP的角色上發(fā)生了一個(gè)反轉(zhuǎn),個(gè)人--->成熟的框架OkHttp即控制反轉(zhuǎn),好處也很明顯,框架出錯(cuò)的概率遠(yuǎn)低于個(gè)人。
2.2 什么是數(shù)據(jù)驅(qū)動(dòng)UI?
通俗一點(diǎn)說(shuō)就是當(dāng)數(shù)據(jù)改變時(shí)對(duì)應(yīng)的UI也要跟著變,反過(guò)來(lái)說(shuō)當(dāng)需要改變UI只需要改變對(duì)應(yīng)的數(shù)據(jù)即可?,F(xiàn)在比較流行的UI框架如Flutter、Compose、Vue其本質(zhì)都是基于函數(shù)式編程實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)UI,它們共同的目的都是為了解決數(shù)據(jù),UI一致性問(wèn)題。
在當(dāng)前的Android中可以使用DataBinding實(shí)現(xiàn)同樣的效果,以Jetpack MVVM為例:ViewModel從Repository拿到數(shù)據(jù)暫存到ViewModel對(duì)應(yīng)的ObservableFiled即可實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)UI,但前提是從Repository拿到的數(shù)據(jù)可以直接用,如果在Activity或者Adapter做數(shù)據(jù)二次處理再notify UI,已經(jīng)違背數(shù)據(jù)驅(qū)動(dòng)UI核心思想。所以想實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)UI必須要有合理的分層(UI層拿到的數(shù)據(jù)無(wú)需處理,可以直接用),Data Mapper恰好解決這一問(wèn)題,同時(shí)也可規(guī)避大量編寫(xiě)BindAdapter的現(xiàn)狀。
DataBinding并非函數(shù)式編程,它只是通過(guò)AbstractProcessor生成中間代碼,將數(shù)據(jù)映射到XML中
2.3 為什么說(shuō)數(shù)據(jù)驅(qū)動(dòng)UI底層思想是控制反轉(zhuǎn)?
當(dāng)前Android生態(tài)能實(shí)現(xiàn)數(shù)據(jù)綁定UI的框架只有兩個(gè):DataBinding、Compose(暫不討論)
在引入DataBinding之前渲染一條數(shù)據(jù)通常需要兩步,如下:
var title = "iOS"
fun setTitle(){
//第一步更改數(shù)據(jù)源
title = "Android"
//第二個(gè)更改UI
textView = title
}
共需要兩步更改數(shù)據(jù)源、更改UI,數(shù)據(jù)源跟UI有一個(gè)忘記修改便會(huì)出現(xiàn)BUG,千萬(wàn)不要說(shuō):“兩個(gè)我都不會(huì)忘記修改”,當(dāng)面臨復(fù)雜的邏輯以及十幾個(gè)甚至幾十個(gè)的數(shù)據(jù)源很難保證不出錯(cuò)。這種問(wèn)題可以通過(guò)DataBinding解決,只需更改對(duì)應(yīng)的ObservableFiledUI便會(huì)同步修改,控制UI狀態(tài)也從個(gè)人反轉(zhuǎn)到的DataBinding,個(gè)人疏忽的事情DataBinding可不會(huì)。
所以說(shuō)數(shù)據(jù)驅(qū)動(dòng)UI底層思想是控制反轉(zhuǎn)
2.4 為什么引入Diff?
引入diff之前:
RecyclerView想要實(shí)現(xiàn)動(dòng)態(tài)刪除、添加、更新需要分別手動(dòng)更新數(shù)據(jù)和UI,這樣在中間插了一道并且分別更新數(shù)據(jù)和UI已經(jīng)違背了前面所說(shuō)的數(shù)據(jù)驅(qū)動(dòng)UI,而我們想要的是不管刪除、添加或者更新只有一個(gè)入口,只要改變數(shù)據(jù)源就會(huì)驅(qū)動(dòng)UI做更新,想要滿(mǎn)足這一原則只能改變數(shù)據(jù)源后對(duì)RecyclerView做全部刷新,但這樣會(huì)造成性能問(wèn)題,復(fù)雜的界面會(huì)感到明顯的卡頓。
引入diff之后:
Diff算法通過(guò)對(duì)oldItem和newItem做差異化比對(duì),會(huì)自動(dòng)更新改變的item,同時(shí)支持刪除、添加的動(dòng)畫(huà)效果,這一特性解決了RecyclerView需要實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)UI的性能問(wèn)題
3 為什么我建議使用 函數(shù)式編程
3.1 什么是 函數(shù)式編程?
一個(gè)入口,一個(gè)出口。 不在函數(shù)鏈內(nèi)部執(zhí)行與運(yùn)算本身無(wú)關(guān)的操作 不在函數(shù)鏈內(nèi)部使用外部變量(實(shí)際上這一條很難遵守,可以適當(dāng)突破)
說(shuō)的通俗點(diǎn)就是給定一個(gè)初始值,經(jīng)過(guò)函數(shù)鏈的運(yùn)行會(huì)得到一個(gè)目標(biāo)值,運(yùn)算的過(guò)程中外部沒(méi)有插手的權(quán)限,同時(shí)不做與本身無(wú)關(guān)的操作,從根本上解決了不可預(yù)期錯(cuò)誤的產(chǎn)生。
舉個(gè)例子:
//Kotlin代碼
listOf(10, 20).map {
it + 1
}.forEach {
Log.i("list", "$it")
}
上面這種鏈?zhǔn)骄幊叹褪菢?biāo)準(zhǔn)的函數(shù)式編程,輸入到輸出之間開(kāi)發(fā)者根本沒(méi)有插手的機(jī)會(huì)(即Log.i(..)之前開(kāi)發(fā)者沒(méi)有權(quán)限處理list),所以整個(gè)流程是100%安全的,RxJava、Flow、鏈?zhǔn)礁唠A函數(shù)都是標(biāo)準(zhǔn)的函數(shù)式編程,它們從規(guī)范層面解決數(shù)據(jù)安全問(wèn)題。所以我建議在Kotlin中 碰到數(shù)據(jù)處理盡量使用鏈?zhǔn)礁唠A函數(shù)(RxJava、Kotlin Flow亦然)。
3.2 Android視圖開(kāi)發(fā)可以借鑒函數(shù)式編程思想
Android視圖開(kāi)發(fā)大都遵循如下流程:請(qǐng)求-->處理數(shù)據(jù)-->渲染UI,這一流程可以借鑒函數(shù)式編程,將請(qǐng)求作為入口,渲染做為出口,在這個(gè)流程中盡量不做與當(dāng)前行為無(wú)關(guān)的事(這也要求ViewModel,Repository中的函數(shù)要符合單一原則)。這樣說(shuō)有點(diǎn)籠統(tǒng),下面舉個(gè)反例:
View{
//刷新
fun refresh(){
ViewModel.load(true)
}
//加載更多
fun loadMore(){
ViewModel.load(false)
}
}
ViewModel{
//加載數(shù)據(jù)
load(isRefresh){
if (isRefresh){
//刷新
}else{
//加載更多
}
}
}
View層有刷新、加載更多兩種行為,load(isRefresh)一個(gè)入口,兩個(gè)出口。面臨的問(wèn)題很明顯,修改刷新或加載更多都會(huì)對(duì)對(duì)方產(chǎn)生影響,違反開(kāi)閉原則中的閉(對(duì)修改關(guān)閉:行為沒(méi)變不準(zhǔn)修改源代碼),導(dǎo)致存在不可預(yù)期的問(wèn)題產(chǎn)生??梢越梃b函數(shù)式編程思想對(duì)其進(jìn)行改進(jìn),將ViewModel的load函數(shù)拆分成refresh和loadMore,這樣刷新和加載更多兩種行為、兩個(gè)入口、兩個(gè)出口互不干涉,通過(guò)函數(shù)的銜接形成兩條獨(dú)立的業(yè)務(wù)鏈條。
函數(shù)式編程可以約束我們寫(xiě)出規(guī)范的代碼,面對(duì)不能使用函數(shù)式編程的場(chǎng)景,我們可以嘗試自我約束往函數(shù)式編程方向靠攏,大致也能實(shí)現(xiàn)相同的效果。
綜上所述
合理的分層可以提升復(fù)用性、降低模塊間耦合性 Data Mapper 可以讓視圖層脫離于后端進(jìn)行開(kāi)發(fā) 復(fù)雜的業(yè)務(wù)邏輯應(yīng)該寫(xiě)到use case中 數(shù)據(jù)驅(qū)動(dòng)UI的本質(zhì)是控制反轉(zhuǎn) 通過(guò)函數(shù)式編程可以寫(xiě)出更加安全的代碼
如果大家對(duì)Jetpack MVVM感興趣歡迎留言。
轉(zhuǎn)自:掘金 Bezier
https://juejin.cn/post/6942464122273398820
