Life doesn’t give you what your want, it gives you what you DESERVE!
0x00 序言
Google 官方在 GitHub 推出的Android 架构 Demo项目非常值得学习,通过 TODO app 的例子,讲解各种应用架构模式如何在实现需求的基础上,达到可维护性、扩展性、可测试性的优秀设计。
本系列博客文章聚焦于以下几点:
- 简明扼要介绍各个架构的思想与基础模型
- 在该架构模式下如何对 TODO App 的各个模块进行划分,以及划分的依据
- 取一个页面为例,作出类图,解析代码
Talk is cheap, show me the code.
0x01 业务逻辑
建议 clone 代码后真机运行体验,用一张图概括如下。
Google 也给出了一个wiki 页面说明 TODO app 的特性。至于为什么选择这样一个应用作为 demo,官方给出的解释如下:
它必须足够简单,便于你很快地理解其功能;同时也要足够复杂,这样才能展现不同设计的决策以及提供测试场景。
0x02 todo-mvp
关于 MVP
MVP 脱胎于大名鼎鼎的 MVC(Model-View-Controller)。
但凡做软件开发的人应该对 MVC 都不感到陌生,它的基本思想是将数据、视图、控制逻辑拆分。这三部分可以自由进行替换,比如在不改变数据接口的情况下,替换数据来源;或者是调整 UI 显示,而无需改动业务逻辑。
MVC 是一个具有历史意义的架构模式,它对天马行空、一团乱麻的软件设计进行规约,使后续对程序的修改和扩展简化,并且使重用成为可能。但是,它并不是最适合 Android 开发的架构模式。
MVC 为什么不适合 Android 开发
MVC 的核心思想是解耦,单一职责。在 Android 开发中,不使用 MVC 的主要原因是 Activity 的职责太重,往往要同时承担 View 和 Controller 的工作,这会造成 Activity 类非常庞大,UI 代码和逻辑代码交织,耦合严重。
MVP 为什么适合 Android 开发
MVP 的架构如下图。
它单独抽出了 Presenter 对象,实现了业务逻辑,用于控制 View 的显示变化,以及与 Model 进行数据交互。Presenter 和 View 互相持有,Model 仅被 Presenter 持有。
在理想状态下,Presenter 和 Model 仅包含 Java 代码,不含 Android SDK 内容。MVP 架构解决了 MVC 中 Activity 职责过多的问题,将 UI 功能分配给 View 单独管理,View 的接口仅向 Presenter 开放,相比 MVC,更好地实现了职责单一、解耦的需求。
如何抽象业务场景
Model
设计从数据层开始,数据层要承担什么职责呢?其实就是CRUD:
- 保存新建的任务
- 查询已创建任务
- 更新任务状态(未完成 -> 已完成)
- 删除任务
数据的持久化方式,数据操作结果采用同步还是异步,这些问题也是设计过程中要考虑的。
基本的任务对象:Task
首先设计一个Task.java
类,表示任务对象,它是一个典型的 Java Bean。在这里我们将 Task 设计成一个不可变的对象。在一个任务从“未完成”变为“完成”状态时,并不是修改原对象,而是丢弃掉原对象,再以相同id
重新创建一个完成状态的对象。这样做的好处是逻辑简单,不可变对象线程绝对安全。缺点是当对象创建、销毁成本高的时候,会产生性能损失。在实际业务中应当酌情选择重用或者舍弃。
1 | public final class Task { |
Model 层对外暴露的接口:TasksDataSource
本着面向接口编程的原则,隐藏数据操作具体实现代码,暴露出管理任务的接口(包含回调)。
1 | public interface TasksDataSource { |
这个接口里有两处与我的编程思路有出入。首先是加载任务回调,我认为可以仅保留复数任务的接口,对于加载单条任务的需求,返回一个长度为 1 的列表就可以了。其次,在命名上,我习惯于将同步接口以getXXX
,异步接口以loadXXX
来声明。
有了这个接口,就可以提供给任务详情、任务列表等页面使用。接下来创建一个接口实现类,这个类对外的职责是实现数据操作接口,对内的职责是隐藏两种具体的数据操作实现(内存缓存、数据库、网络)。
数据源共有三级缓存:
- In-memory cache - Fast
- Disk (SQLiteDb) - Slow
- Network - Very slow
1 | public class TasksRepository implements TasksDataSource { |
读写数据策略可以概括为“依次读,全部写”,括号里是我补充的内容:
In every
get
operation:- Return cache if available, or
- return local copy if it exists (and update cache) , or
- return remote copy ( and update local copy & cache)
Every
write
/delete
operation will simply:- Update cache
- Update local
- Update remote
具体的网络数据源、本地数据库设计为常见写法,不赘述。
Fragment 即是 View
处理所有 UI 变化事件,比如显示 loading、显示任务列表、显示任务详情、显示任务状态变更 SnackBar 等,值得注意的是页面跳转操作也属于 UI 变化,是在 Fragment 里面实现的。不妨思考一下为什么“跳转”不放在 Presenter 中进行?一个原因是 Presenter 与 Android SDK 无关,而跳转需要 Context 对象,违背了这一原则。比如从“任务列表”跳转到“任务详情”时:
1 | // TasksFragment.java |
View 里面没有任何业务逻辑,主要处理以下事件:
- 加载中、加载成功、失败页面展示
- 展现 Toast、SnackBar 等提示
- 处理按钮、菜单点击事件(通常是将其甩给 Presenter 处理)
- 处理页面跳转
- 声明 Adapter
- 在
onResume
时调用 Presenter 的start
方法,启动页面 - 提供
isActive
方法,供 Presenter 在异步回调返回时判断页面是否存活
最后一点是容易漏掉的,如果异步请求回调时页面被销毁,会导致各种难以预料的问题。
1 | public boolean isActive() { |
Presenter - 业务逻辑载体
承担了业务逻辑实现的职责,有时业务逻辑过于复杂会导致 Presenter 类太大,此时要考虑拆分业务逻辑到单独的类中,就变成了mvp-clean
架构,clean 架构中使用Use Case(用例)
处理细分的业务逻辑。
Presenter 由 Activity 构造,并且提供方法给 Activity 以及 View(Fragment)使用。
Activity - 将一切粘合到一起
现在我们有了 Model、View、Presenter 各个部分,而 Android 应用的页面入口是 Activity 类,我们在 Activity 中创建 Fragment(View)及 Presenter,在这两者之间建立关联。同时还要处理一些必须放在 Activity 中进行的操作,比如onSaveInstanceState
、onOptionsItemSelected
等等。
任务列表功能实现
用类图来表示 MVP 模式下的任务列表功能相关类: