Android真響應(yīng)式架構(gòu)——Model層設(shè)計

前言

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)行:

  1. 數(shù)據(jù)過濾
  2. 數(shù)據(jù)加工
  3. 接口拆分
  4. 接口合并
  5. 等等

數(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è)計是極其類似的:

數(shù)據(jù)庫層和網(wǎng)絡(luò)層

差別僅在于,我們可以通過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便是一種屬性委托),因此對于UserPreferencestoken屬性的訪問最終還是會由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)

如上圖所示,這是最終的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ù)庫的數(shù)據(jù)中間層,將數(shù)據(jù)庫CURD直接暴露給Repository。

移除所有數(shù)據(jù)中間層

任意數(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è)計思路:

  1. 數(shù)據(jù)以流的形式呈現(xiàn)(不包括SharedPreferences)
  2. 屏蔽底層“原操作”的細(xì)節(jié)
  3. 以數(shù)據(jù)的實際需求為導(dǎo)向(上文所說的數(shù)據(jù)邏輯)
  4. 統(tǒng)一數(shù)據(jù)接口,屏蔽數(shù)據(jù)來源差異
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容