美文网首页Android开发经验谈Android
Android真响应式架构——Model层设计

Android真响应式架构——Model层设计

作者: 珞泽珈群 | 来源:发表于2019-01-29 17:34 被阅读77次

    前言

    Android真响应式架构系列文章:

    Android真响应式架构——MvRx
    Epoxy——RecyclerView的绝佳助手
    Android真响应式架构——Model层设计

    之前我介绍了Airbnb的响应式架构MvRx,以及它界面响应式的关键——Epoxy。从这篇文章开始,我会写几篇文章来介绍一下我应用MvRx的一些实践。
    这篇文章是关于Model层设计的,对,就是MVC、MVP、MVVM中的那个Model。其实,Model层的设计和响应式架构没有关系。但是,因为这是一系列的文章,为了统一,我还是这么命名了。

    本篇介绍的Model层设计与响应式架构无关,别的架构同样可以参考这样的设计。

    本文介绍的一切都基于一点:数据流的设计,也即以RxJava的方式包装Model层的数据。希望你熟悉RxJava。

    1. Model层的分层

    优秀的架构离不开合理的分层设计,我们经常说的MVC、MVP、MVVM正是从大的方面描述了整体架构的分层模式。然而,仅仅在大的方面做好分层还是远远不够的,每一层本身也可能是非常复杂的,在每一层内部还要进行细分。因此,我们需要对Model层进行进一步的细分设计。

    1.1 网络层的分层设计

    相信大家对于网络层采用Retrofit+RxJava的方案应该没有什么异议,甚至Retorfit都不必强求,只要网络层的数据是以RxJava数据流的形式提供的即可。不过,下面我仍然会使用Retrofit来举例。

    1.1.1 数据过滤层

    如果网络层的数据不是“纯净”的,我们第一步应该做的事情是去除“噪声”。假设后台的数据都是以如下的JSON形式返回给我们的:

    {
        "status": 200,
        "data": "我是String"
    }
    
    {
        "status": 200,
        "data": {
            //我是JSONObject
        }
    }
    
    {
        "status": 200,
        "data": [
            //我是JSONArray
        ]
    }
    

    以上这种接口设计还是很常见的,我们真正需要的数据保存在data字段中,所以我们这里设计一个数据过滤层,拿到我们真正关心的数据,然后再做别的处理。

    /**
     * 网络返回的数据
     */
    class StatusSuccess<T>(
        val status: Int = 0,
        val data: T
    )
    
    /**
     * Retrofit接口
     */
    interface UserApi {
        /**
         * 获取用户信息
         */
        @GET
        fun getUserInfo(): Observable<StatusSuccess<UserInfo>>
        
        /**
         * 常见问题
         */
        @GET
        fun faq(): Observable<StatusSuccess<List<FAQ>>>
    
        /**
         * 清空消息
         */
        @DELETE
        fun clearNotices(): Observable<StatusSuccess<String>>
    }
    
    /**
     * 数据过滤层
     */
    interface UserService {
        fun getUserInfo(): Observable<UserInfo>
        
        fun faq(): Observable<List<FAQ>>
    
        fun clearNotices(): Observable<String>
    }
    
    /**
     * 对网络请求返回的数据类型进行转换,StatusSuccess<T> -> T
     */
    inline fun <reified T> unwrapData() = Function<StatusSuccess<T>, T> {
        it.data as T
    }
    
    /**
     * 真正的网络请求实现类
     */
    @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())
    }
    

    首先定义网络数据的泛型表示类StatusSuccess<T>,还有Retrofit网络请求接口UserApi,然后定义一个数据过滤层UserService,主要作用是将StatusSuccess<T>转换为T,只保留我们真正关心的数据(无论数据是String,还是数据类,抑或是List),最后,在UserClient中实现UserService接口,实现真正的网络请求。

    1.1.2 数据过滤层->数据中间层

    如果只是为了过滤“噪声”的话,加一层数据过滤层似乎也没有太大的意义,直接使用UserApi也未尝不可。但是,数据过滤层的作用还不止如此。由于作用以及发生了变化,所以我把它改称为数据中间层。
    举个例子,假设后台把收藏、取消收藏写成了一个接口,通过一个叫type的参数区分是收藏还是取消收藏:

    interface UserApi {
        //...
        
        /**
         * type 1收藏 2取消收藏
         */
        @FormUrlEncoded
        @POST
        fun collectSomething(@Field("id") id: Int, @Field("type") type: Int): Observable<StatusSuccess<String>>
    }
    

    但是,如果其它层调用这个方法还需要传入一个type的话,这就不太友好的,毕竟有写错的风险,即使没写错,也需要在传入参数的时候查看一下到底type是几的时候代表收藏。总之,这样的网络层使用不便。其实,可以通过数据中间层来屏蔽这个问题。

    /**
     * 数据中间层
     */
    interface UserService {
        //如果只是数据过滤的话我们会这么定义
        fun collectSomething(id: Int, type: Int): Observable<String>
        
        //但是,不应该局限于数据过滤,因此,我们这么定义
        //收藏
        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())
    }
    

    将数据过滤层升级为数据中间层,把收藏和取消收藏定义为两个方法(虽然在底层它们调用的是同一个方法)。通过这样的拆分,网络层会变得更加易用,也更不易犯错。对于网络层的使用者而言,就好像后台真的有两个接口一样。
    其实,无论是叫数据过滤层也好,数据中间层也好,这一层的职责是很明确的,就是以数据实际需求的角度去定义数据接口。从这个角度出发,这一层可以发挥更多的作用。
    回顾之前的例子,由于我们只需要StatusSuccess<T>中的data字段,所以我们过滤掉了不必要的数据;由于我们需要收藏和取消收藏两种数据接口,所以我们定义了两个接口。以数据的实际需求为导向的话,你会发现你可以在数据中间层进行:

    1. 数据过滤
    2. 数据加工
    3. 接口拆分
    4. 接口合并
    5. 等等

    数据过滤和接口拆分在上文中已经提到过了。数据加工的情形就更多了,后台返回的数据总会有不能直接使用的情况,这时,在数据中间层以你实际需求的数据定义一个接口,然后在诸如UserClient的类中进行数据处理就可以了(通常就是map或者doOnNext一下)。对于网络层的使用者而言,就好像后台返回的数据本身就是这样的一样,拿来就用,不需要额外的处理。
    接口合并也非常常见。例如,注册之后直接登录,但是后台的的注册接口却不返回登录接口的数据:

    interface UserApi {
        /**
         * 登录
         */
        @POST
        fun login(...): Observable<StatusSuccess<LoginData>>
    
        /**
         * 注册
         */
        @POST
        fun register(...): Observable<StatusSuccess<RegisterData>>
    }
    
    interface UserService {
        fun register(...): Observable<LoginData>
    }
    
    @Singleton
    class UserClient @Inject constructor(
        private val userApi: UserApi
    ) : UserService {
    
        override fun register(...): Observable<LoginData> =
            userApi.register(...).flatMap(login(...)).map(unwrapData())
    }
    

    管你register方法原来返回的是啥,我需要的是LoginData,然后在UserClient中通过flatMap操作符将后台注册、登录两个接口串行起来。有串行就有并行,多个接口并行可以采用zip等操作符。
    接口的合并还可以有别的含义,例如,将我们之前举得收藏、取消收藏的例子反过来。后台对于两个相似的操作定义了两个接口,然而我们却想在使用的时候,当成一个接口使用:

    interface UserApi {
        
        /**
         * 收藏
         */
        @FormUrlEncoded
        @POST
        fun collectSomething(@Field("id") id: Int): Observable<StatusSuccess<String>>
    
        /**
         * 取消收藏
         */
        @FormUrlEncoded
        @POST
        fun unCollectSomething(@Field("id") id: Int): Observable<StatusSuccess<String>>
    }
    
    /**
     * 数据中间层
     */
    interface UserService {
        //收藏、取消收藏
        //可以在这一层为参数提供默认值
        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())
    }
    

    上面这个例子可能不太合适,这个例子只是为了说明数据中间层定义的灵活性,一切以方便使用为导向,你可以在这一层进行很多设计。

    1.1.3 网络层设计总结

    网络层以RxJava数据流的形式暴露出原始的网络请求数据,然后通过数据中间层提供给其它层使用。数据中间层是以数据的实际需求为目的而定义的,我们可以在这一层对数据进行任意的组合、拆分、加工。这样,对于网络层的使用者而言,就好像后台数据压根儿就是这样的,拿来即用,不需多余的处理。这对于屏蔽“操蛋”后端而言真是极好的,数据中间层仿佛变成了后端不可逾越的一道屏障,从这一层往后将是“一马平川”的前端世界,一个由我们完全掌控的世界。

    1.2 数据库的分层设计

    除了网络数据,有时候应用还需要本地数据库的支持。优秀的数据库ORM框架有很多,我也没用过几个。这里不局限于某种ORM框架,只从较高的抽象层级谈谈数据库的分层设计。
    从Model层之外的角度来看,数据是来源于远程网络还是来源于本地数据库是没有区别的,数据库层的设计可以借鉴网络层的设计。数据库的CURD对应于网络层的API,然后也是通过数据中间层向其它部分提供服务。
    假设需要通过本地数据库记录用户的搜索信息,需要记录最近的10条搜索信息。

    /**
     * 数据库CURD基本操作
     */
    interface SearchDao {
        //获取搜索记录
        fun getSearchHistory(count: Int): Observable<List<String>>
    
        //保存的搜索记录数
        fun searchHistoryCount(): Int
    
        //清空搜索记录
        fun clearSearchHistory()
    
        //插入搜索记录
        fun insertSearchHistory(searchKey: String)
    
        //删除搜索记录,saveCount表示保留几条
        fun deleteSearchHistory(saveCount: Int)
    }
    
    /**
     * 数据中间层
     */
    interface SearchService {
    
        fun getSearchHistory(): Observable<List<String>>
    
        fun clearSearchHistory()
    
        fun insertSearch(searchKey: String)
    }
    
    /**
     * 真正的数据库实现类
     */
    @Singleton
    class SearchClient @Inject constructor(
        private val searchDao: SearchDao
    ) : SearchService {
        //显示出来的搜索记录
        private val showCount = 10
        //限制数据库存储的最大记录数
        private val maxSaveCount = 50
    
        override fun getArticleSearchHistory(): Observable<List<String>> =
            return searchDao.getSearchHistory(showCount)
    
        override fun clearSearchHistory() {
            searchDao.clearSearchHistory()
        }
    
        /**
        * 当数据库存储的搜索记录大于10条时,不必要每次都删除旧的记录
        * 直到数据记录达到最大限制时,再一起删除所有旧的记录
        */
        override fun insertSearch(searchKey: String) {
            searchDao.insertSearchHistory(searchKey)
            if (searchDao.searchHistoryCount() > maxSaveCount) {
                searchDao.deleteSearchHistory(showCount)
            }
        }
    
    }
    

    注释已经讲得很清楚了,延迟删除搜索记录,一直到达到最大限再进行统一删除。之所以这么做是想表明,不应该将数据库的基本操作CURD暴露出来提供给其它层使用(尤其在数据库比较复杂时),而应该通过数据中间层进行抽象,以实际数据需求为导向定义数据中间层,屏蔽数据库基本操作,通过数据中间层,仅对外提供数据逻辑的接口。对上述例子而言就是,insertSearch不仅包含了数据库插入操作,可能还包含了查询记录数量、删除记录的操作,我们应该在数据中间层实现这些细节,对外仅提供insertSearch这一数据逻辑接口。
    数据库层的分层设计和网络层的分层设计是极其类似的:

    数据库层和网络层

    差别仅在于,我们可以通过CURD直接操作本地数据库,而对于远程的数据库,我们只能通过后台提供的网络API进行操作。对于本地数据库而言,CURD是其“原操作”;而对于远程数据库而言,网络API是其“原操作”。所以说,数据中间层还可以这么理解,不应该将数据的“原操作”直接暴露出来,因为这些“原操作”可能太过底层,需要进行组合、拆分、变换等操作之后,数据才能变得可用、易用。这些细节应该通过数据中间层进行屏蔽,对外提供更加“高级”的数据逻辑接口。

    说到组合、拆分、变换我想起了孙悟空的七十二变,说到孙悟空,明年下半年,中美合拍,文体两开花。呸,这台词太六了,我控制不住寄己。 说到组合、拆分、变换这不就是RxJava的拿手好戏,所以,RxJava才是把这一切串联起来的关键。

    1.3 SharedPreferences的封装

    除了网络数据,数据库数据,SharedPreferences更是不可或缺的。由于SharedPreferences提供数据的方式比较简单,并且可以在主线程中获取,关于SharedPreferences似乎并不需要太多封装,拿来直接用就行了。其实,也并非完全如此,结合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()
        }
    }
    
    /**
     * 数据中间层(还是这么称呼吧)
     */
    interface UserPreferences {
        var token: String
        //...
    }
    
    /**
     * 实现类
     */
    @Singleton
    class MyPreferences @Inject constructor(
        sharedPreferences: SharedPreferences
    ) : UserPreferences {
        override var token: String by PreferenceDelegate(sharedPreferences, "sp_token", "")
    }
    
    

    PreferenceDelegate是个属性委托类。简单来说就是把对某个类某个属性的访问委托给另一个类来实现(Kotlin中常用的by lazy便是一种属性委托),因此对于UserPreferencestoken属性的访问最终还是会由SharedPreferences完成,只是这一切都是由属性委托帮我们完成的,如此这般,对于SharedPreferences的读写完全变换成了对于UserPreferences中属性的访问,一切都不着痕迹。

    2. 数据仓库

    如上,我们已经构建好了网络层,数据库层,也封装好了SharedPreferences。其实,这样就可以直接供其它层使用了。但是,正如前面提到的,站在Model层之外,数据是来源于网络还是数据库是没有任何区别的,为了屏蔽这两者之间的差异,我们需要再增加一层,称为数据仓库,它将所有数据汇总,对外屏蔽数据来源的差异。

    /**
     * 数据仓库
     */
    @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的另外一个特性——委托(不是属性委托),委托帮我们减少了大量的样本代码,让数据仓库的定义变得异常简洁。数据仓库并非仅仅只是将各个接口委托出去,它可以包含很多内容,例如,数据缓存;数据库和网络数据的结合(先访问数据库,再访问网络,网络数据保存到数据库等),可以根据自己的需求实现,这里就不再举例了。
    数据仓库并非只能有一个,例如你可以为“我的”定义一个UserRepository的数据仓库,还可以为“发现”定义一个FindRepository的数据仓库,等等。

    Model层结构

    如上图所示,这是最终的Model层的结构,所有数据的操作都是通过数据中间层进行的。Repository的主要职责是对外提供无差异的数据接口,在Kotlin委托的帮助下,Repository的实现变得异常简单,我们只需要选择性的覆写特定的接口,完成诸如数据缓存、数据结合等工作即可。
    整个Model层的构建需要创建非常多的对象,并且有比较复杂的依赖关系,这些都是通过Dagger2进行统一管理的(以上代码中均有所体现)。

    3. 如何简化

    上面给出了完整的Model层的结构,整体上层级结构还是很清晰的,也不算复杂。但是,有时候完整地实现这套结构还是略显繁琐。现实的需求是千变万化的,没必要拘泥于某种特性的模式。前面已经说过了,数据中间层是为了屏蔽“原操作”,提供数据逻辑接口。但是在数据比较简单的情况下,“原操作”有时候就等同于数据逻辑。譬如说,在数据库很简单的情况下,我们只需要一个基本的查询/插入等操作就可以完成我们的需求,数据库的CURD就等同于我们需要的数据逻辑,在这种情况下,并不需要什么数据中间层。

    移除数据库数据中间层

    移除数据库的数据中间层,将数据库CURD直接暴露给Repository。

    移除所有数据中间层

    任意数据源的数据中间层都可以移除,直接连接到Repository上。

    总结

    以上是我个人在开发实践中使用的Model层的设计,可能有不成熟的地方,仅供大家参考。
    总结一下Model层的设计思路:

    1. 数据以流的形式呈现(不包括SharedPreferences)
    2. 屏蔽底层“原操作”的细节
    3. 以数据的实际需求为导向(上文所说的数据逻辑)
    4. 统一数据接口,屏蔽数据来源差异

    相关文章

      网友评论

        本文标题:Android真响应式架构——Model层设计

        本文链接:https://www.haomeiwen.com/subject/lzoxsqtx.html