詳解Android官方架構(gòu)中UseCase
1. UseCase 的用途
Android 最新的架構(gòu)規(guī)范中,引入了 Domain Layer(譯為領(lǐng)域?qū)觨r網(wǎng)域?qū)樱ㄗh大家使用 UseCase 來封裝一些復(fù)雜的業(yè)務(wù)邏輯。
傳統(tǒng)的 MVVM 架構(gòu)中,我們習(xí)慣用 ViewModel 來承載業(yè)務(wù)邏輯,隨著業(yè)務(wù)規(guī)模的擴(kuò)大,ViewModel 變得越來越肥大,職責(zé)不清。
Clean Architecture 提出的關(guān)注點(diǎn)分離和單一職責(zé)(SRP)的設(shè)計原則被廣泛認(rèn)可,因此 Android 在最新架構(gòu)中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 歸屬 UI Layer,更加聚焦 UiState 的管理,UI 無關(guān)的業(yè)務(wù)邏輯下沉 UseCase,UseCase 與 ViewModel 解耦后,也可以跨 ViewModel 提供公共邏輯。
Android 架構(gòu)早期的示例代碼 todo-app 中曾經(jīng)引入過 UseCase 的概念,最新架構(gòu)中只不過是將 UseCase 的思想更明確了,最新的 UseCase 示例可以從官方的 NIA 中學(xué)習(xí)。
2. UseCase 的特點(diǎn)
官方文檔認(rèn)為 UseCase 應(yīng)該具有以下幾個特點(diǎn):
2.1 不持有狀態(tài)
可以定義自己的數(shù)據(jù)結(jié)構(gòu)類型,但是不能持有狀態(tài)實例,像一個純函數(shù)一樣工作。甚至直接推薦大家將邏輯重寫到 invoke 方法中,像調(diào)用函數(shù)一樣調(diào)用實例。
下面是 NIA 中的一個示例:GetRecentSearchQueriesUseCase
:
2.2 單一職責(zé)
嚴(yán)格遵守單一職責(zé),一個 UseCase 只做一件事情,甚至其命名就是一個具體行為。掃一眼 UseCase 的文件目錄大概就知道 App 的大概功能了。
下面 NIA 中所有 UseCases:
2.3 可有可無
官方文檔中將 UseCase 定義為可選的角色,按需定義。簡單的業(yè)務(wù)場景中允許 UI 直接訪問 Repository。如果我們將 UseCase 作為 UI 與 Data 隔離的角色,那么工程中會出現(xiàn)很多沒有太大價值的 UseCase ,可能就只有一行調(diào)用 Repoitory 的代碼。
3. 如何定義 UseCase
如上所述,官方文檔雖然對 UseCase 給出了一些基本定義,但是畢竟是一個新新生概念,很多人在真正去寫代碼的時候仍然會感覺不清晰,缺少有效指引。在究竟如何定義 UseCase 這個問題上,還有待大家更廣泛的討論,形成可參考的共識。本文也是帶著這個目的而生,算是拋磚引玉吧。
3.1 Optional or Mandatory?
首先,官方文檔認(rèn)為 UseCase 是可選的,雖然其初衷是好的,大家都不希望出現(xiàn)太多 One-Liner 的 UseCase,但是作為一個架構(gòu)規(guī)范切忌模棱兩可,這種“可有可無”的規(guī)則其結(jié)局往往就是“無”。
業(yè)務(wù)剛起步時由于比較簡單往往定義在 Repository 中,隨著業(yè)務(wù)規(guī)模的擴(kuò)大,應(yīng)該適當(dāng)?shù)迷黾?UseCase 封裝一些復(fù)雜的業(yè)務(wù)邏輯,但是實際項目中此時的重構(gòu)成本會讓開發(fā)者變得“懶惰”,UseCase 最終難產(chǎn)。
那放棄 UseCase 呢?這可能會造成 Repository 的職責(zé)不清和無限膨脹,而且 Repository 往往不止有一個方法, ViewModel 直接依賴 Repository 也違反了 SOLID 中的另一個重要原則 ISP ,ViewModel 會因為不相關(guān)的 Repository 改動導(dǎo)致重新編譯。
ISP(Interface Segregation Principle,接口隔離原則) 要求將接口分離成更小的和更具體的接口,以便調(diào)用方只需知道其需要使用的方法。這可以提高代碼的靈活性和可重用性,并減少代碼的依賴性和耦合性。
為了降低前期判斷成本和后續(xù)重構(gòu)成本,如果我們有業(yè)務(wù)持續(xù)壯大的預(yù)期,那不妨考慮將 UseCase 作為強(qiáng)制選項。當(dāng)然,最好這需要研究如何降低 UseCase 帶來的模板代碼。
3.2 Class or Object?
官方建議使用 Class
定義 UseCase,每次使用都實例化一個新對象,這會做成一些重復(fù)開銷,那么可否用 object
定義 UseCase 呢?
UseCase 理論上可以作為單例存在,但 Class 相對于 Object 有以下兩個優(yōu)勢:
- UseCase 希望像純函數(shù)一樣工作,普通 Class 可以確保每次使用時都會創(chuàng)建一個新的實例,從而避免狀態(tài)共享和副作用等問題。
- 普通類可以通過構(gòu)造參數(shù)注入不同的 Repository,UseCase 更利于復(fù)用和單元測試
如果我們強(qiáng)烈希望 UseCase 有更長的生命周期,那借助 DI 框架,普通類也可以簡單的支持。例如 Dagger 中只要添加 @Singleton
注解即可
@Singleton class GetRecentSearchQueriesUseCase @Inject constructor( private val recentSearchRepository: RecentSearchRepository, ) { operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> = recentSearchRepository.getRecentSearchQueries(limit) }
3.3 Class or Function?
既然我們想像函數(shù)一樣使用 UseCase ,那為什么不直接定義成 Function
呢?比如像下面這樣
fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>>
這確實遵循了 FP 的原則,但又喪失了 OOP 封裝性的優(yōu)勢:
- UseCase 往往需要依賴 Repository 對象,一個 UseCase Class 可以將 Repository 封裝為成員存儲。而一個 UseCase Function 則需要調(diào)用方通過參數(shù)傳入,使用成本高不說,如果 UseCase 依賴的 Repository 的類型或者數(shù)量發(fā)生變化了,調(diào)用方需要跟著修改
- 函數(shù)起不到隔離 UI 和 Data 的作用,ViewModel 仍然需要直接依賴 Repository,為 UseCase 傳參
- UseCase Class 可以定義一些 private 的方法,相對于 Function 更能勝任一些復(fù)雜邏輯的實現(xiàn)
可見,在 UseCase 的定義上 Function 沒法取代 Class。當(dāng)然 Class 也帶來一些弊端:
- 暴露多個方法,破壞 SRP 原則。所以官方推薦用
verb in present tense + noun/what (optional) + UseCase
動詞命名,也是想讓職責(zé)更清晰。 - 攜帶可變狀態(tài),這是大家寫 OOP 的慣性思維
- 樣板代碼多
3.4 Function interface ?
通過前面的分析我們知道:UseCase 的定義需要兼具 FP 和 OOP 的優(yōu)勢。這讓我想到了 Function(SAM) Interface 。Function Interface 是一個單方法的接口,可以低成本創(chuàng)建一個匿名類對象,確保對象只能有一個方法,同時具有一定封裝性,可以通過“閉包”依賴 Repository。此外,Kotlin 對 SAM 提供了簡化寫法,一定程度也減少了樣板代碼。
Functional (SAM) interfaces: kotlinlang.org/docs/fun-in…
改用 Function interface 定義 GetRecentSearchQueriesUseCase 的代碼如下:
fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>
用它創(chuàng)建 UseCase 實例的同時,實現(xiàn)函數(shù)中的邏輯
val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase { //... }
我在函數(shù)實現(xiàn)中如何 Repository 呢?這要靠 DI 容器獲取。官方示例代碼中都使用 Hilt 來解耦 ViewModel 與 UseCase 的,ViewModel 不關(guān)心 UseCase 的創(chuàng)建細(xì)節(jié)。下面是 NIA 的代碼, GetRecentSearchQueriesUseCase 被自動注入到 SearchViewModel
中。
@HiltViewModel class SearchViewModel @Inject constructor( recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM //... ) : ViewModel() { //... }
Function interface 的 GetRecentSearchQueriesUseCase 沒有構(gòu)造函數(shù),需要通過 Dagger 的 @Module
安裝到 DI 容器中,provideGetRecentSearchQueriesUseCase
參數(shù)中的 RecentSearchRepository 可以從容器中自動獲取使用。
@Module @InstallIn(ActivityComponent::class) object UseCaseModule { @Provides fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) = GetRecentSearchQueriesUseCase { limit -> recentSearchRepository.getRecentSearchQueries(limit) } }
當(dāng)時用 Koin 作為 DI 容器時也沒問題,代碼如下:
single<GetRecentSearchQueriesUseCase> { GetRecentSearchQueriesUseCase { limit -> recentSearchRepository.getRecentSearchQueries(limit) } }
4. 總結(jié)
UseCase 作為官方架構(gòu)中的新概念,尚沒有完全深入人心,需要不斷探索合理的使用方式,本文給出一些基本思考:
- 考慮到架構(gòu)的擴(kuò)展性,推薦在 ViewModel 與 Repository 之間強(qiáng)制引入 UseCase,即使眼下的業(yè)務(wù)邏輯并不復(fù)雜
- UseCase 不持有可變狀態(tài)但依賴 Repository,需要兼具 FP 與 OOP 的特性,更適合用 Class 定義而非 Function
- 在引入 UseCase 之前應(yīng)該先引入 DI 框架,確保 ViewModel 與 UseCase 的耦合。
- Function Interface 是 Class 之外的另一種定義 UseCase 的方式,有利于代碼更加函數(shù)式
以上就是詳解Android官方架構(gòu)中UseCase的詳細(xì)內(nèi)容,更多關(guān)于Android官方架構(gòu)UseCase 的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android開發(fā)中ProgressDialog簡單用法示例
這篇文章主要介紹了Android開發(fā)中ProgressDialog簡單用法,結(jié)合實例形式分析了Android使用ProgressDialog的進(jìn)度條顯示與關(guān)閉、更新等事件響應(yīng)相關(guān)操作技巧,需要的朋友可以參考下2017-10-10Android編程實現(xiàn)google消息通知功能示例
這篇文章主要介紹了Android編程實現(xiàn)google消息通知功能,結(jié)合具體實例形式分析了Android消息處理及C#服務(wù)器端與google交互的相關(guān)操作技巧,需要的朋友可以參考下2017-06-06Android在多種設(shè)計下實現(xiàn)懶加載機(jī)制的方法
這篇文章主要介紹了Android在多種設(shè)計下實現(xiàn)懶加載機(jī)制的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化
這篇文章主要為大家介紹了Android性能優(yōu)化getResources()與Binder導(dǎo)致界面卡頓優(yōu)化示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Android PopupWindow被輸入法彈上去之后無法恢復(fù)原位的解決辦法
這篇文章主要介紹了Android PopupWindow被輸入法彈上去之后無法恢復(fù)原位的解決辦法,需要的朋友可以參考下2016-12-12Android中ScrollView監(jiān)聽滑動距離案例講解
這篇文章主要介紹了Android中ScrollView監(jiān)聽滑動距離案例講解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08