前言

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

Android真响应式架构——MvRx
Epoxy——RecyclerView的绝佳助手
Android真响应式架构——Model层设计
Android真响应式架构——数据流动性
Android真响应式架构——Epoxy的使用
Android真响应式架构——MvRx和Epoxy的结合

之前我介绍了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 StatusData(    val status: Int = 0,    val data: T)/** * Retrofit接口 */interface UserApi {    /**     * 获取用户信息     */    @GET    fun getUserInfo(): Observable>        /**     * 常见问题     */    @GET    fun faq(): Observable>>    /**     * 清空消息     */    @DELETE    fun clearNotices(): Observable>}/** * 数据过滤层 */interface UserService {    fun getUserInfo(): Observable        fun faq(): Observable>    fun clearNotices(): Observable}/** * 对网络请求返回的数据类型进行转换,StatusData -> T */inline fun  unwrapData() = Function, T> {    it.data as T}/** * 真正的网络请求实现类 */@Singletonclass UserClient @Inject constructor(    private val userApi: UserApi) : UserService {    override fun getUserInfo(): Observable =        userApi.getUserInfo().map(unwrapData())    override fun faq(): Observable> =        userApi.faq().map(unwrapData())    override fun clearNotices(): Observable =        userApi.clearNotices().map(unwrapData())}

首先定义网络数据的泛型表示类StatusData,还有Retrofit网络请求接口UserApi,然后定义一个数据过滤层UserService,主要作用是将StatusData转换为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>}

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

/** * 数据中间层 */interface UserService {    //如果只是数据过滤的话我们会这么定义    fun collectSomething(id: Int, type: Int): Observable        //但是,不应该局限于数据过滤,因此,我们这么定义    //收藏    fun collectSomething(id: Int): Observable    //取消收藏    fun unCollectSomething(id: Int): Observable}@Singletonclass UserClient @Inject constructor(    private val userApi: UserApi) : UserService {    //...        override fun collectSomething(id: Int): Observable =        userApi.collectSomething(id, 1).map(unwrapData())            override fun unCollectSomething(id: Int): Observable =        userApi.unCollectSomething(id, 2).map(unwrapData())}

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

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

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

interface UserApi {    /**     * 登录     */    @POST    fun login(...): Observable>    /**     * 注册     */    @POST    fun register(...): Observable>}/** * 数据中间层 */interface UserService {    fun register(...): Observable}@Singletonclass UserClient @Inject constructor(    private val userApi: UserApi) : UserService {    override fun register(...): Observable =        userApi.register(...).flatMap(userApi.login(...)).map(unwrapData())}

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

interface UserApi {        /**     * 收藏     */    @FormUrlEncoded    @POST    fun collectSomething(@Field("id") id: Int): Observable>    /**     * 取消收藏     */    @FormUrlEncoded    @POST    fun unCollectSomething(@Field("id") id: Int): Observable>}/** * 数据中间层 */interface UserService {    //收藏、取消收藏    //可以在这一层为参数提供默认值    fun collectSomething(id: Int, isCollected: Boolean = true): Observable}@Singletonclass UserClient @Inject constructor(    private val userApi: UserApi) : UserService {    override fun collectSomething(id: Int, isCollected: Boolean): Observable =        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>    //保存的搜索记录数    fun searchHistoryCount(): Int    //清空搜索记录    fun clearSearchHistory()    //插入搜索记录    fun insertSearchHistory(searchKey: String)    //删除搜索记录,saveCount表示保留几条    fun deleteSearchHistory(saveCount: Int)}/** * 数据中间层 */interface SearchService {    fun getSearchHistory(): Observable>    fun clearSearchHistory()    fun insertSearch(searchKey: String)}/** * 真正的数据库实现类 */@Singletonclass SearchClient @Inject constructor(    private val searchDao: SearchDao) : SearchService {    //显示出来的搜索记录    private val showCount = 10    //限制数据库存储的最大记录数    private val maxSaveCount = 50    override fun getArticleSearchHistory(): Observable> =        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这一数据逻辑接口。
数据库层的分层设计和网络层的分层设计是极其类似的:

Android真响应式架构——Model层设计_第1张图片 数据库层和网络层

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

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

1.3 SharedPreferences的封装

除了网络数据,数据库数据,SharedPreferences更是不可或缺的。由于SharedPreferences提供数据的方式比较简单,并且可以在主线程中获取,关于SharedPreferences似乎并不需要太多封装,拿来直接用就行了。其实,也并非完全如此,结合Kotlin,SharedPreferences的使用将变得更加简单,也更加不着痕迹。
Kotlin有个特性叫做属性委托,特别适合SharedPreferences的使用情形:

/** * 对于SharedPreferences的访问可以委托给该类 * 通过default的类型判断属性的类型 */class PreferenceDelegate(        private val sharedPref: SharedPreferences,        val name: String,        private val default: T) : ReadWriteProperty {    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  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  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    //...}/** * 实现类 */@Singletonclass 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层之外,数据是来源于网络还是数据库是没有任何区别的,为了屏蔽这两者之间的差异,我们需要再增加一层,称为数据仓库,它将所有数据汇总,对外屏蔽数据来源的差异。

/** * 数据仓库 */@Singletonclass 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的数据仓库,等等。

Android真响应式架构——Model层设计_第2张图片 Model层结构

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

3. 如何简化

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

Android真响应式架构——Model层设计_第3张图片 移除数据库数据中间层

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

Android真响应式架构——Model层设计_第4张图片 移除所有数据中间层

任意数据源的数据中间层都可以移除,直接连接到Repository上。我建议,还是要保留Repository,对外提供统一的数据逻辑接口,屏蔽数据源差异(即使你没有使用数据库,只有网络数据,也推荐这么做),不要把底层的数据直接暴露出来。

总结

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

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

更多相关文章

  1. Android创建和使用数据库详细指南
  2. android 使用Intent传递数据之全局变量传递
  3. Android真响应式架构——数据流动性
  4. 处女男学Android(十四)---Android 重量级数据存储之SQLite
  5. Android——数据存储(Login)
  6. android 读取串口数据的服务,android串口
  7. Android gson解析json数据工具类

随机推荐

  1. Android新版NDK环境配置(免Cygwin)
  2. Android中设计模式无处不在之单例模式
  3. android之xml数据解析(DOM)
  4. Android(安卓)上实现非root的 Traceroute
  5. Emulator(2)Choose and Configure AVD an
  6. mldn android
  7. Android(安卓)Animations 3D flip
  8. Android(安卓)博文积累
  9. android数据五种存储
  10. linux32位下使用Android提示 ERROR: 32-b