前言
Android真響應(yīng)式架構(gòu)系列文章:
Android真響應(yīng)式架構(gòu)——MvRx
Epoxy——RecyclerView的絕佳助手
Android真響應(yīng)式架構(gòu)——Model層設(shè)計
Android真響應(yīng)式架構(gòu)——數(shù)據(jù)流動性
Android真響應(yīng)式架構(gòu)——Epoxy的使用
Android真響應(yīng)式架構(gòu)——MvRx和Epoxy的結(jié)合
之前我介紹了Airbnb的響應(yīng)式架構(gòu)MvRx,以及它界面響應(yīng)式的關(guān)鍵——Epoxy。從這篇文章開始,我會寫幾篇文章來介紹一下,我應(yīng)用MvRx的一些實踐。
這篇文章是關(guān)于Model層設(shè)計的,對,就是MVC、MVP、MVVM中的那個Model。其實,Model層的設(shè)計和響應(yīng)式架構(gòu)沒有關(guān)系。但是,因為這是一系列的文章,為了統(tǒng)一,我還是這么命名了。
本篇介紹的Model層設(shè)計與響應(yīng)式架構(gòu)無關(guān),別的架構(gòu)同樣可以參考這樣的設(shè)計。
本文介紹的一切都基于一點:數(shù)據(jù)流的設(shè)計,即以RxJava的方式包裝Model層的數(shù)據(jù),然后進(jìn)行合理的數(shù)據(jù)分層,以實現(xiàn)對數(shù)據(jù)流的分層管控。因此,希望你熟悉RxJava。
1. Model層的分層
優(yōu)秀的架構(gòu)離不開合理的分層設(shè)計,我們經(jīng)常說的MVC、MVP、MVVM正是從大的方面描述了整體架構(gòu)的分層模式。然而,僅僅在大的方面做好分層還是遠(yuǎn)遠(yuǎn)不夠的,每一層本身也可能是非常復(fù)雜的,在每一層內(nèi)部還要進(jìn)行細(xì)分。因此,我們需要對Model層進(jìn)行進(jìn)一步的細(xì)分設(shè)計。
1.1 網(wǎng)絡(luò)層的分層設(shè)計
相信大家對于網(wǎng)絡(luò)層采用Retrofit+RxJava的方案應(yīng)該沒有什么異議,甚至Retorfit都不必強(qiáng)求,只要網(wǎng)絡(luò)層的數(shù)據(jù)是以RxJava數(shù)據(jù)流的形式提供的即可。不過,下面我仍然會使用Retrofit來舉例。
1.1.1 數(shù)據(jù)過濾層
如果網(wǎng)絡(luò)層的數(shù)據(jù)不是“純凈”的,我們第一步應(yīng)該做的事情是去除“噪聲”。假設(shè)后臺的數(shù)據(jù)都是以如下的JSON形式返回給我們的:
{
"status": 200,
"data": "我是String"
}
{
"status": 200,
"data": {
//我是JSONObject
}
}
{
"status": 200,
"data": [
//我是JSONArray
]
}
以上這種接口設(shè)計還是很常見的,我們真正需要的數(shù)據(jù)保存在data
字段中,所以我們這里設(shè)計一個數(shù)據(jù)過濾層,拿到我們真正關(guān)心的數(shù)據(jù),然后再做別的處理。
/**
* 網(wǎng)絡(luò)返回的數(shù)據(jù)
*/
class StatusData<T>(
val status: Int = 0,
val data: T
)
/**
* Retrofit接口
*/
interface UserApi {
/**
* 獲取用戶信息
*/
@GET
fun getUserInfo(): Observable<StatusData<UserInfo>>
/**
* 常見問題
*/
@GET
fun faq(): Observable<StatusData<List<FAQ>>>
/**
* 清空消息
*/
@DELETE
fun clearNotices(): Observable<StatusData<String>>
}
/**
* 數(shù)據(jù)過濾層
*/
interface UserService {
fun getUserInfo(): Observable<UserInfo>
fun faq(): Observable<List<FAQ>>
fun clearNotices(): Observable<String>
}
/**
* 對網(wǎng)絡(luò)請求返回的數(shù)據(jù)類型進(jìn)行轉(zhuǎn)換,StatusData<T> -> T
*/
inline fun <reified T> unwrapData() = Function<StatusData<T>, T> {
it.data as T
}
/**
* 真正的網(wǎng)絡(luò)請求實現(xiàn)類
*/
@Singleton
class UserClient @Inject constructor(
private val userApi: UserApi
) : UserService {
override fun getUserInfo(): Observable<UserInfo> =
userApi.getUserInfo().map(unwrapData())
override fun faq(): Observable<List<FAQ>> =
userApi.faq().map(unwrapData())
override fun clearNotices(): Observable<String> =
userApi.clearNotices().map(unwrapData())
}
首先定義網(wǎng)絡(luò)數(shù)據(jù)的泛型表示類StatusData<T>
,還有Retrofit網(wǎng)絡(luò)請求接口UserApi
,然后定義一個數(shù)據(jù)過濾層UserService
,主要作用是將StatusData<T>
轉(zhuǎn)換為T
,只保留我們真正關(guān)心的數(shù)據(jù)(無論數(shù)據(jù)是String,還是數(shù)據(jù)類,抑或是List),最后,在UserClient
中實現(xiàn)UserService
接口,實現(xiàn)真正的網(wǎng)絡(luò)請求。
1.1.2 數(shù)據(jù)過濾層->數(shù)據(jù)中間層
如果只是為了過濾“噪聲”的話,加一層數(shù)據(jù)過濾層似乎也沒有太大的意義,直接使用UserApi
也未嘗不可。但是,數(shù)據(jù)過濾層的作用還不止如此。由于作用以及發(fā)生了變化,所以我把它改稱為數(shù)據(jù)中間層。
舉個例子,假設(shè)后臺把收藏、取消收藏寫成了一個接口,通過一個叫type
的參數(shù)區(qū)分是收藏還是取消收藏:
interface UserApi {
//...
/**
* type 1收藏 2取消收藏
*/
@FormUrlEncoded
@POST
fun collectSomething(@Field("id") id: Int, @Field("type") type: Int): Observable<StatusData<String>>
}
但是,如果其它層調(diào)用這個方法還需要傳入一個type
的話,這就不太友好的,畢竟有寫錯的風(fēng)險,即使沒寫錯,也需要在傳入?yún)?shù)的時候查看一下到底type
是幾的時候代表收藏。總之,這樣的網(wǎng)絡(luò)層使用不便。其實,可以通過數(shù)據(jù)中間層來屏蔽這個問題。
/**
* 數(shù)據(jù)中間層
*/
interface UserService {
//如果只是數(shù)據(jù)過濾的話我們會這么定義
fun collectSomething(id: Int, type: Int): Observable<String>
//但是,不應(yīng)該局限于數(shù)據(jù)過濾,因此,我們這么定義
//收藏
fun collectSomething(id: Int): Observable<String>
//取消收藏
fun unCollectSomething(id: Int): Observable<String>
}
@Singleton
class UserClient @Inject constructor(
private val userApi: UserApi
) : UserService {
//...
override fun collectSomething(id: Int): Observable<String> =
userApi.collectSomething(id, 1).map(unwrapData())
override fun unCollectSomething(id: Int): Observable<String> =
userApi.unCollectSomething(id, 2).map(unwrapData())
}
將數(shù)據(jù)過濾層升級為數(shù)據(jù)中間層,把收藏和取消收藏定義為兩個方法(雖然在底層它們調(diào)用的是同一個方法)。通過這樣的拆分,網(wǎng)絡(luò)層會變得更加易用,也更不易犯錯。對于網(wǎng)絡(luò)層的使用者而言,就好像后臺真的有兩個接口一樣。
其實,無論是叫數(shù)據(jù)過濾層也好,數(shù)據(jù)中間層也好,這一層的職責(zé)是很明確的,就是以數(shù)據(jù)實際需求的角度去定義數(shù)據(jù)接口。從這個角度出發(fā),這一層可以發(fā)揮更多的作用。
回顧之前的例子,由于我們只需要StatusData<T>
中的data
字段,所以我們過濾掉了不必要的數(shù)據(jù);由于我們需要收藏和取消收藏兩種數(shù)據(jù)接口,所以我們定義了兩個接口。以數(shù)據(jù)的實際需求為導(dǎo)向的話,你會發(fā)現(xiàn)你可以在數(shù)據(jù)中間層進(jìn)行:
- 數(shù)據(jù)過濾
- 數(shù)據(jù)加工
- 接口拆分
- 接口合并
- 等等
數(shù)據(jù)過濾和接口拆分在上文中已經(jīng)提到過了。數(shù)據(jù)加工的情形就更多了,后臺返回的數(shù)據(jù)總會有不能直接使用的情況,這時,在數(shù)據(jù)中間層以你實際需求的數(shù)據(jù)定義一個接口,然后在諸如UserClient
的類中進(jìn)行數(shù)據(jù)處理就可以了(通常就是map
或者doOnNext
一下)。對于網(wǎng)絡(luò)層的使用者而言,就好像后臺返回的數(shù)據(jù)本身就是這樣的一樣,拿來就用,不需要額外的處理。
接口合并也非常常見。例如,注冊之后直接登錄,但是后臺的的注冊接口卻不返回登錄接口的數(shù)據(jù):
interface UserApi {
/**
* 登錄
*/
@POST
fun login(...): Observable<StatusData<LoginData>>
/**
* 注冊
*/
@POST
fun register(...): Observable<StatusData<RegisterData>>
}
/**
* 數(shù)據(jù)中間層
*/
interface UserService {
fun register(...): Observable<LoginData>
}
@Singleton
class UserClient @Inject constructor(
private val userApi: UserApi
) : UserService {
override fun register(...): Observable<LoginData> =
userApi.register(...).flatMap(userApi.login(...)).map(unwrapData())
}
管你register
方法原來返回的是啥,我需要的是LoginData
,然后在UserClient
中通過flatMap
操作符將后臺注冊、登錄兩個接口串行起來就OK了。有串行就有并行,多個接口并行可以采用zip
等操作符。
接口的合并還可以有別的含義,例如,將我們之前舉得收藏、取消收藏的例子反過來。后臺對于兩個相似的操作定義了兩個接口,然而我們卻想在使用的時候,當(dāng)成一個接口使用:
interface UserApi {
/**
* 收藏
*/
@FormUrlEncoded
@POST
fun collectSomething(@Field("id") id: Int): Observable<StatusData<String>>
/**
* 取消收藏
*/
@FormUrlEncoded
@POST
fun unCollectSomething(@Field("id") id: Int): Observable<StatusData<String>>
}
/**
* 數(shù)據(jù)中間層
*/
interface UserService {
//收藏、取消收藏
//可以在這一層為參數(shù)提供默認(rèn)值
fun collectSomething(id: Int, isCollected: Boolean = true): Observable<String>
}
@Singleton
class UserClient @Inject constructor(
private val userApi: UserApi
) : UserService {
override fun collectSomething(id: Int, isCollected: Boolean): Observable<String> =
if (isCollected)
userApi.collectSomething(id).map(unwrapData())
else
userApi.unCollectSomething(id).map(unwrapData())
}
上面這個例子可能不太合適,這個例子只是為了說明數(shù)據(jù)中間層定義的靈活性,一切以方便使用為導(dǎo)向,你可以在這一層進(jìn)行很多設(shè)計。
1.1.3 網(wǎng)絡(luò)層設(shè)計總結(jié)
網(wǎng)絡(luò)層以RxJava數(shù)據(jù)流的形式暴露出原始的網(wǎng)絡(luò)請求數(shù)據(jù),然后通過數(shù)據(jù)中間層提供給其它層使用。數(shù)據(jù)中間層是以數(shù)據(jù)的實際需求為目的而定義的,我們可以在這一層對數(shù)據(jù)進(jìn)行任意的組合、拆分、加工。這樣,對于網(wǎng)絡(luò)層的使用者而言,就好像后臺數(shù)據(jù)壓根兒就是這樣的,拿來即用,不需多余的處理。這對于屏蔽“操蛋”后端而言真是極好的,數(shù)據(jù)中間層仿佛變成了后端不可逾越的一道屏障,從這一層往后將是“一馬平川”的前端世界,一個由我們完全掌控的世界。
1.2 數(shù)據(jù)庫的分層設(shè)計
除了網(wǎng)絡(luò)數(shù)據(jù),有時候應(yīng)用還需要本地數(shù)據(jù)庫的支持。優(yōu)秀的數(shù)據(jù)庫ORM框架有很多,我也沒用過幾個。這里不局限于某種ORM框架,只從較高的抽象層級談?wù)剶?shù)據(jù)庫的分層設(shè)計。
從Model層之外的角度來看,數(shù)據(jù)是來源于遠(yuǎn)程網(wǎng)絡(luò)還是來源于本地數(shù)據(jù)庫是沒有區(qū)別的,數(shù)據(jù)庫層的設(shè)計可以借鑒網(wǎng)絡(luò)層的設(shè)計。數(shù)據(jù)庫的CURD對應(yīng)于網(wǎng)絡(luò)層的API,然后也是通過數(shù)據(jù)中間層向其它部分提供服務(wù)。
假設(shè)需要通過本地數(shù)據(jù)庫記錄用戶的搜索信息,需要記錄最近的10條搜索信息。
/**
* 數(shù)據(jù)庫CURD基本操作
*/
interface SearchDao {
//獲取搜索記錄
fun getSearchHistory(count: Int): Observable<List<String>>
//保存的搜索記錄數(shù)
fun searchHistoryCount(): Int
//清空搜索記錄
fun clearSearchHistory()
//插入搜索記錄
fun insertSearchHistory(searchKey: String)
//刪除搜索記錄,saveCount表示保留幾條
fun deleteSearchHistory(saveCount: Int)
}
/**
* 數(shù)據(jù)中間層
*/
interface SearchService {
fun getSearchHistory(): Observable<List<String>>
fun clearSearchHistory()
fun insertSearch(searchKey: String)
}
/**
* 真正的數(shù)據(jù)庫實現(xiàn)類
*/
@Singleton
class SearchClient @Inject constructor(
private val searchDao: SearchDao
) : SearchService {
//顯示出來的搜索記錄
private val showCount = 10
//限制數(shù)據(jù)庫存儲的最大記錄數(shù)
private val maxSaveCount = 50
override fun getArticleSearchHistory(): Observable<List<String>> =
return searchDao.getSearchHistory(showCount)
override fun clearSearchHistory() {
searchDao.clearSearchHistory()
}
/**
* 當(dāng)數(shù)據(jù)庫存儲的搜索記錄大于10條時,不必要每次都刪除舊的記錄
* 直到數(shù)據(jù)記錄達(dá)到最大限制時,再一起刪除所有舊的記錄
*/
override fun insertSearch(searchKey: String) {
searchDao.insertSearchHistory(searchKey)
if (searchDao.searchHistoryCount() > maxSaveCount) {
searchDao.deleteSearchHistory(showCount)
}
}
}
注釋已經(jīng)講得很清楚了,延遲刪除搜索記錄,一直到達(dá)到最大限再進(jìn)行統(tǒng)一刪除。之所以這么做是想表明,不應(yīng)該將數(shù)據(jù)庫的基本操作CURD暴露出來提供給其它層使用(尤其在數(shù)據(jù)庫比較復(fù)雜時),而應(yīng)該通過數(shù)據(jù)中間層進(jìn)行抽象,以實際數(shù)據(jù)需求為導(dǎo)向定義數(shù)據(jù)中間層,屏蔽數(shù)據(jù)庫基本操作,通過數(shù)據(jù)中間層,僅對外提供數(shù)據(jù)邏輯的接口。對上述例子而言就是,insertSearch
不僅包含了數(shù)據(jù)庫插入操作,可能還包含了查詢記錄數(shù)量、刪除記錄的操作,我們應(yīng)該在數(shù)據(jù)中間層實現(xiàn)這些細(xì)節(jié),對外僅提供insertSearch
這一數(shù)據(jù)邏輯接口。
數(shù)據(jù)庫層的分層設(shè)計和網(wǎng)絡(luò)層的分層設(shè)計是極其類似的:
差別僅在于,我們可以通過CURD直接操作本地數(shù)據(jù)庫,而對于遠(yuǎn)程的數(shù)據(jù)庫,我們只能通過后臺提供的網(wǎng)絡(luò)API進(jìn)行操作。對于本地數(shù)據(jù)庫而言,CURD是其“原操作”;而對于遠(yuǎn)程數(shù)據(jù)庫而言,網(wǎng)絡(luò)API是其“原操作”。所以說,數(shù)據(jù)中間層還可以這么理解,不應(yīng)該將數(shù)據(jù)的“原操作”直接暴露出來,因為這些“原操作”可能太過底層,需要進(jìn)行組合、拆分、變換等操作之后,數(shù)據(jù)才能變得可用、易用。這些細(xì)節(jié)應(yīng)該通過數(shù)據(jù)中間層進(jìn)行屏蔽,對外提供更加“高級”的數(shù)據(jù)邏輯接口。
說到組合、拆分、變換我想起了孫悟空的七十二變,說到孫悟空,明年下半年,中美合拍,文體兩開花。呸,這臺詞太六了,我控制不住寄己。說到組合、拆分、變換這不就是RxJava的拿手好戲,所以,RxJava才是把這一切串聯(lián)起來的關(guān)鍵。
1.3 SharedPreferences的封裝
除了網(wǎng)絡(luò)數(shù)據(jù),數(shù)據(jù)庫數(shù)據(jù),SharedPreferences更是不可或缺的。由于SharedPreferences提供數(shù)據(jù)的方式比較簡單,并且可以在主線程中獲取,關(guān)于SharedPreferences似乎并不需要太多封裝,拿來直接用就行了。其實,也并非完全如此,結(jié)合Kotlin,SharedPreferences的使用將變得更加簡單,也更加不著痕跡。
Kotlin有個特性叫做屬性委托,特別適合SharedPreferences的使用情形:
/**
* 對于SharedPreferences的訪問可以委托給該類
* 通過default的類型判斷屬性的類型
*/
class PreferenceDelegate<T>(
private val sharedPref: SharedPreferences,
val name: String,
private val default: T
) : ReadWriteProperty<Any?, T> {
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return getPreference(name, default)
}
override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
@Suppress("UNCHECKED_CAST")
private fun <T> getPreference(name: String, default: T): T = with(sharedPref) {
val res: Any = when (default) {
is String -> getString(name, default)
is Boolean -> getBoolean(name, default)
is Int -> getInt(name, default)
is Float -> getFloat(name, default)
is Long -> getLong(name, default)
else -> throw IllegalArgumentException("type can't be saved into SharedPreferences")
}
res as T
}
private fun <T> putPreference(name: String, value: T) = with(sharedPref.edit()) {
when (value) {
is String -> putString(name, value)
is Boolean -> putBoolean(name, value)
is Int -> putInt(name, value)
is Float -> putFloat(name, value)
is Long -> putLong(name, value)
else -> throw IllegalArgumentException("type can't be saved into SharedPreferences")
}.apply()
}
}
/**
* 數(shù)據(jù)中間層(還是這么稱呼吧)
*/
interface UserPreferences {
var token: String
//...
}
/**
* 實現(xiàn)類
*/
@Singleton
class MyPreferences @Inject constructor(
sharedPreferences: SharedPreferences
) : UserPreferences {
override var token: String by PreferenceDelegate(sharedPreferences, "sp_token", "")
}
PreferenceDelegate
是個屬性委托類。簡單來說就是把對某個類某個屬性的訪問委托給另一個類來實現(xiàn)(Kotlin中常用的by lazy便是一種屬性委托),因此對于UserPreferences
中token
屬性的訪問最終還是會由SharedPreferences完成,只是這一切都是由屬性委托幫我們完成的,如此這般,對于SharedPreferences的讀寫完全變換成了對于UserPreferences
中屬性的訪問,一切都不著痕跡。
2. 數(shù)據(jù)倉庫
如上,我們已經(jīng)構(gòu)建好了網(wǎng)絡(luò)層,數(shù)據(jù)庫層,也封裝好了SharedPreferences。其實,這樣就可以直接供其它層使用了。但是,正如前面提到的,站在Model層之外,數(shù)據(jù)是來源于網(wǎng)絡(luò)還是數(shù)據(jù)庫是沒有任何區(qū)別的,為了屏蔽這兩者之間的差異,我們需要再增加一層,稱為數(shù)據(jù)倉庫,它將所有數(shù)據(jù)匯總,對外屏蔽數(shù)據(jù)來源的差異。
/**
* 數(shù)據(jù)倉庫
*/
@Singleton
class UserRepository @Inject constructor(
private val userClient: UserClient,
private val searchClient: SearchClient,
private val preferences: MyPreferences
) : UserService by userClient,
SearchService by searchClient,
UserPreferences by preferences
這里利用了Kotlin的另外一個特性——委托(不是屬性委托),委托幫我們減少了大量的樣板代碼,讓數(shù)據(jù)倉庫的定義變得異常簡潔。數(shù)據(jù)倉庫并非僅僅只是將各個接口委托出去,它可以包含很多內(nèi)容,例如,數(shù)據(jù)緩存;數(shù)據(jù)庫和網(wǎng)絡(luò)數(shù)據(jù)的結(jié)合(先訪問數(shù)據(jù)庫,再訪問網(wǎng)絡(luò),網(wǎng)絡(luò)數(shù)據(jù)保存到數(shù)據(jù)庫等),可以根據(jù)自己的需求實現(xiàn),這里就不再舉例了。
數(shù)據(jù)倉庫并非只能有一個,例如你可以為“我的”定義一個UserRepository
的數(shù)據(jù)倉庫,還可以為“發(fā)現(xiàn)”定義一個FindRepository
的數(shù)據(jù)倉庫,等等。
如上圖所示,這是最終的Model層的結(jié)構(gòu),所有數(shù)據(jù)的操作都是通過數(shù)據(jù)中間層進(jìn)行的。Repository的主要職責(zé)是對外提供無差異的數(shù)據(jù)接口,在Kotlin委托的幫助下,Repository的實現(xiàn)變得異常簡單,我們只需要選擇性的覆寫特定的接口,完成諸如數(shù)據(jù)緩存、數(shù)據(jù)結(jié)合等工作即可。
整個Model層的構(gòu)建需要創(chuàng)建非常多的對象,并且有比較復(fù)雜的依賴關(guān)系,這些都是通過Dagger2進(jìn)行統(tǒng)一管理的(以上代碼中均有所體現(xiàn))。
3. 如何簡化
上面給出了完整的Model層的結(jié)構(gòu),整體上層級結(jié)構(gòu)還是很清晰的,也不算復(fù)雜。但是,有時候完整地實現(xiàn)這套結(jié)構(gòu)還是略顯繁瑣。現(xiàn)實的需求是千變?nèi)f化的,沒必要拘泥于某種特定的模式。前面已經(jīng)說過了,數(shù)據(jù)中間層是為了屏蔽“原操作”,提供數(shù)據(jù)邏輯接口。但是在數(shù)據(jù)比較簡單的情況下,“原操作”有時候就等同于數(shù)據(jù)邏輯。譬如說,在數(shù)據(jù)庫很簡單的情況下,我們只需要一個基本的查詢/插入等操作就可以完成我們的需求,數(shù)據(jù)庫的CURD就等同于我們需要的數(shù)據(jù)邏輯,在這種情況下,并不需要什么數(shù)據(jù)中間層。
移除數(shù)據(jù)庫的數(shù)據(jù)中間層,將數(shù)據(jù)庫CURD直接暴露給Repository。
任意數(shù)據(jù)源的數(shù)據(jù)中間層都可以移除,直接連接到Repository上。我建議,還是要保留Repository,對外提供統(tǒng)一的數(shù)據(jù)邏輯接口,屏蔽數(shù)據(jù)源差異(即使你沒有使用數(shù)據(jù)庫,只有網(wǎng)絡(luò)數(shù)據(jù),也推薦這么做),不要把底層的數(shù)據(jù)直接暴露出來。
總結(jié)
以上是我個人在開發(fā)實踐中使用的Model層的設(shè)計,可能有不成熟的地方,僅供大家參考。
總結(jié)一下Model層的設(shè)計思路:
- 數(shù)據(jù)以流的形式呈現(xiàn)(不包括SharedPreferences)
- 屏蔽底層“原操作”的細(xì)節(jié)
- 以數(shù)據(jù)的實際需求為導(dǎo)向(上文所說的數(shù)據(jù)邏輯)
- 統(tǒng)一數(shù)據(jù)接口,屏蔽數(shù)據(jù)來源差異