MVWTF:揭秘建筑模式
模型-视图-控制器
模型-视图-演示器
模型-视图-视图模型
模型-视图-意图
概要
那我应该用什么呢?
代码示例
最初发表于Android Essence。
您可以在 YouTube 上观看Android Summit的这段演讲:
作为一名 Android 开发人员,我经常在社区中看到有人问“我应该使用哪种架构模式?”
这场讨论通常会引出一些晦涩难懂的缩写词:
- MVC
- 最有价值球员
- MVVM
- MVI
- MVU?(我们不谈论这个,但它显然是新来的游戏)
对于刚入行的安卓开发者来说,这可能会让人感到不知所措,经验丰富的开发者也会不断质疑自己是否选对了工具。无论你是正在纠结该学习哪款工具,还是想知道自己目前使用的工具是否最适合你,这篇文章都能帮助你做出正确的选择。
我们首先应该理解为什么需要架构模式。当这个问题被提出时,我们又会听到更多空洞的术语,比如我们想要的代码应该是这样的:
- 可维护的
- 可扩展
- 强壮的
- 可测试的
乍听之下,这些词似乎并不像流行语,但实际上往往只是空话。编写可维护的代码究竟意味着什么?“健壮”一词意味着强大且健康。那么,什么是强大且健康的代码呢?
我们将从头开始。抛开所有时髦的术语,我们需要从一个基本事实入手,这也是本文其余部分的基础:
你不能把所有的代码都放在 Activity 里。
我们其实都心知肚明,所以会把代码放到不同的文件中。然而,这不仅仅是把各种类放在各自的文件里那么简单。如何拆分代码至关重要,而架构模式正是用来解决这个问题的。架构模式就是描述如何拆分代码的一种方式。
让我们从头开始,逐一分析代码拆分的几种方法,首先处理首字母缩写词列表。
模型-视图-控制器
MVC 值得用一小段篇幅来介绍,因为它历史最悠久的架构模式之一。它诞生于 20 世纪 70 年代,旨在将应用程序拆分为三个组件:模型、视图和控制器。理解每个组件的功能至关重要。
模型
模型组件就是你的数据源。它可以是数据库、远程服务器、本地文本文件或其他任何来源。需要记住的重要一点是,它与视图无关。
你的模型不应该负责任何与数据显示方式相关的行为,而应该只负责检索数据。
例如,如果您想获取一个用户并显示一个包含用户名和年龄的标签,则需要在其他地方创建该标签,而不是在模型组件内部创建。
看法
视图组件是数据的可视化呈现,它只负责这一点。它不应该关心数据来源,也不应该做出任何决策。如果你发现自己在视图组件中编写了条件逻辑,请考虑重构这段代码。
控制器
作为最后一个组件,控制器几乎负责其他所有事情。它应该:
- 处理用户输入
- 如有必要,请验证输入内容。
- 将该输入传递给模型
- 将该模型响应传递给视图
理解这个流程的一个简单方法是把它想象成一个表单。控制器读取所有文本输入,确保所有内容都已填写,然后将其发送给模型,最后指示用户界面显示成功页面。
MVC图
让我们来看看这一切是如何联系起来的:
为什么我们不在安卓系统上使用这项技术?
表面上看,这似乎相当不错。我们的关注点被区分开来,信息流也相当清晰。
然而,在本文提到的所有模式中,MVC 在 Android 上的讨论最少。对此,我稍作解释。
当你考虑控制器(接收用户输入)和视图(显示数据)的职责时,你会发现它们在 Android 中是由同一个东西来处理的。那就是 Activity 或 Fragment。
为什么这样做不好?
我想重点说明我们不希望在安卓系统上实现此功能的两个原因:
- 我们无法为 Activity 或 Fragment 编写 JUnit 测试,因此我们应该尽可能多地将代码移出其中。
- 如果三个组件中有两个属于同一类,那么我们关注的问题实际上并没有分开。
我们该如何解决这个问题?
让我们把控制器/UI逻辑从Activity/Fragment中移出来。
模型-视图-演示器
通过将用户界面和业务逻辑从 Activity/Fragment 中分离出来,我们创建了一种略有不同的信息流。这就形成了 MVP 模式。
现在我们实现了关注点分离,所有非 UI 代码都位于 Activity/Fragment 之外,我们可以对所有内容进行单元测试。
MVP实施
在介绍下一个模式之前,我认为有必要了解一下 MVP 等模式的实现方式,这样我们就可以比较代码是如何逐步演变的。
合同类别
在 MVP 模式下构建功能的第一件事是设计一个契约类,该类包含定义三个组件行为的接口:
class TaskListContract {
interface View {
fun showTasks(tasks: List<Task>)
}
interface Presenter {
fun viewCreated()
fun viewDestroyed()
}
interface Model {
fun getTasks(): List<Task>
}
}
模型
在这个例子中,我们的模型可以是一个简单的内存列表。
请注意,我们的模型实际上并没有引用其他组件。
class InMemoryTaskService : TaskListContract.Model {
override fun getTasks(): List<Task> {
return listOf(...)
}
}
看法
这里视图实际上只需要做两件事:
- 告知演示者任何相关的生命周期方法,或者如果相关,点击收听者。
- 重写合约类中的任何方法来显示数据
请注意,我们的视图仅引用了 Presenter。
class TaskListActivity : AppCompatActivity(), TaskListContract.View {
private val presenter = TaskListPresenter(this, TaskRepository())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
presenter.viewCreated()
}
override fun onDestroy() {
presenter.viewDestroyed()
super.onDestroy()
}
override fun showTasks(tasks: List<Task>) {
taskAdapter.tasks = tasks
}
}
主持人
演示者需要重写合约类中的方法,在必要时调用视图,并进行任何必要的清理以避免内存泄漏(例如删除对视图的引用)。
请注意,我们的演示器既引用了视图,又引用了模型。
class TaskListPresenter(
private var view: TaskListContract.View?,
private val model: TaskListContract.Model
) : TaskListContract.Presenter {
override fun viewCreated() {
val tasks = model.getTasks()
view?.showTasks(tasks)
}
override fun viewDestroyed() {
view = null
}
}
这样够好吗?
- 我们的视图仅负责显示数据。
- 我们的模型负责数据获取。
- 演示者处理所有输入和用户界面逻辑
- 所有功能都已明确分离,并且所有功能均可测试。
- 如果你觉得这样就足够好了,那就用它吧!
MVVM 有何不同之处?
我们最初列表中的每一种架构模式都会引出下一种。MVP 是我们讨论 MVVM 的基础,因为它们之间实际上只有一个细微的差别:MVP 中的展示者不需要关心视图。
模型-视图-视图模型
在上一种模式中,我们看到 presenter 明确地告诉 view 要显示什么。作为一种替代方案,我们可以考虑基于事件的方法,在这种方法中,我们只需公开 view 应该处于的状态,任何需要显示该状态的人都可以订阅这些状态的改变。
MVVM 的工作原理正是如此。它打破了 presenter 和 view 之间的直接联系,而是通过某种可观察类型(例如 LiveData 或 RxJava)来公开信息。
MVVM实现
让我们来看一下 MVP 和 MVVM 实现之间的代码对比。
模型
模型组件实际上变化不大。不过,由于我们不再使用契约类,我仍然建议为数据获取行为设置一个接口。
interface TaskRepository {
fun getTasks(): List<Task>
}
class InMemoryTaskService : TaskRepository {
override fun getTasks(): List<Task> {
return listOf(...)
}
}
看法
该视图的行为与上一个例子类似,我们需要:
- 创建对我们 ViewModel 的引用
- 观察 ViewModel 的变化,并据此更新 UI。
class TaskListActivity : AppCompatActivity() {
private val viewModel = TaskListviewModel(repository = InMemoryTaskService())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
subscribeToViewModel()
}
private fun subscribeToViewModel() {
viewModel.getTasks().observe(this, Observer { tasks ->
adapter.tasks = tasks
})
}
}
视图模型
我们的 ViewModel 与 Presenter 类似,但有一些关键区别:
- 我们不再引用视图,只有模型组件。
- 我们通过 LiveData 对象公开信息。
- 我们可以在初始化后立即获取信息,因此无需等待视图创建通知。
class TaskListViewModel(
private val repository: TaskRepository
) {
private val tasks = MutableLiveData<List<Task>>()
fun getTasks(): LiveData<List<Task>> = tasks
init {
fetchTasks()
}
private fun fetchTasks() {
tasks.value = repository.getTasks()
}
}
MVVM 比 MVP 有哪些优势?
到目前为止,我们的代码与之前完全相同,只是不再直接引用视图。取消这种连接的好处在于,我们现在可以利用 Android 的ViewModel架构组件来更好地处理配置更改。
在 MVP 版本中,我们的演示者引用了一个视图,该视图通常是一个 Activity。如果我旋转手机,该 Activity 会被重新创建,此时演示者引用的是一个已不存在的 Activity。
有了 ViewModel,我们就拥有了能够经受住配置更改考验的东西,并且永远不会丢失状态。
MVP中的手柄旋转
在MVP版本中,我们需要遵循很多步骤来处理轮换:
- 在我们的主持人合同中添加两种新方法,
getState()并restoreState() - 更新我们的视图,以便在相应的生命周期步骤中调用这些方法。
- 实现用于检索状态并在 presenter 中恢复状态的方法
根据你所处状态的复杂程度,步骤 3 可能需要很长时间和大量的代码。
MVVM 中的手柄旋转
为了确保我们能够轻松地在 MVVM 中处理旋转,我们执行以下操作:
- 更新我们的 ViewModel 以扩展 Android ViewModel 架构组件
- 在我们的 Activity 中,使用 ViewModelProviders 获取已存在的 ViewModel。
- 重新订阅实时数据以获取状态
前两步与上一节的步骤难度相同,但现在我们无需考虑状态的保存和恢复,只需重新订阅 LiveData 即可。
这为我们节省了大量时间。
如果你想查看代码对比,请查看当初发布这篇文章时的幻灯片。
为什么 MVVM 还不够好?
随着我们逐步了解这些架构模式,每一种都显得更加出色。现在我们不仅拥有了MVP架构的所有分离性和可测试性优势,而且还实现了更好的状态轮换管理!
不过,我们的列表中还有一个缩写词,它破坏了MVVM并非我们所能做到的最佳方案的乐趣。为了理解它的不足之处,让我们来看一个更复杂的情况。
假设有一个功能,用于获取任务列表,并支持加载状态和错误状态。您可以尝试将状态放入 Kotlin 的密封类中:
sealed class TaskListState {
object Loading : TaskListState()
data class Loaded(val tasks: List<Task>) : TaskListState()
data class Error(val error: Throwable?) : TaskListState()
}
接下来,你需要更新 ViewModel 以根据实际情况显示状态:
class TaskListViewModel(private val repository: TaskRepository) : ViewModel() {
init {
showLoading()
try {
fetchTasks()
} catch (e: Exception) {
showError()
}
}
private fun showLoading() {
state.value = TaskListState.Loading
}
private fun fetchTasks() {
val tasks = repository.getItems()
state.value = TaskListState.Loaded(tasks)
}
private fun showError() {
state.value = TaskListState.Error(Throwable("Unable to fetch tasks."))
}
}
showLoading()这看起来相当不错。不过,这些方法fetchTasks()也存在一些风险showError()。
- 类中的任何方法都可以调用它们。
- 我们无法保证它们与特定行为或意图相关。
- 我们有多种操作状态的方法,必须确保这些方法彼此之间不冲突。
对于像这样的简单示例,MVVM 可能就足够了。我可以轻松地测试所有内容,确保按正确的顺序调用这些方法,从而避免这些风险。然而,随着 ViewModel 变得越来越复杂,这些风险也会越来越普遍。下一个模式可以解决这个问题。
模型-视图-意图
与之前的模式不同,“意图”指的不是某个特定的组件,而是我们想要在状态中捕获的执行某些操作的意图。
使用 Reducer 管理可预测的状态变化
MVVM 的第一个问题是状态变化缺乏可预测性。与其使用多个方法来操作状态,不如使用一个单一的流水线。这个流水线接收一个操作和一个当前状态,并输出一个新的状态。
该方法的签名如下所示:
abstract class Reducer {
abstract fun reduce(action: Action, state: State): State
}
如果你像我们之前看到的那样使用密封类来表示状态,我们就可以非常清晰地定义输入和输出:
class TaskListReducer : Reducer<TaskListState>() {
override fun reduce(action: Action, state: TaskListState): TaskListState {
return when (action) {
is TaskListAction.TasksLoading -> TaskListState.Loading()
is TaskListAction.TasksLoaded -> TaskListState.Loaded(action.tasks)
is TaskListAction.TasksErrored -> TaskListState.Error()
else -> state
}
}
}
如果未使用密封类,我们也可以利用copy()数据类中的方法来处理状态更新:
class TaskListReducer : Reducer<TaskListState>() {
override fun reduce(action: BaseAction, state: TaskListState): TaskListState {
return when (action) {
is TaskListAction.TasksLoading -> state.copy(
isLoading = true,
isError = false,
tasks = null
)
is TaskListAction.TasksLoaded -> state.copy(
isLoading = false,
isError = false,
tasks = action.tasks
)
is TaskListAction.TasksErrored -> state.copy(
isLoading = false,
isError = true,
tasks = null
)
else -> state
}
}
}
使用单一数据源
我们希望通过 MVI 实现的另一个目标是为状态建立单一数据源。我们可以通过创建一个名为 `state` 的状态容器来实现这一点Store。它具有以下职责:
- 该商店保留了对我们当前状态的参考。
- 该商店保存着对我们减法器的引用
- 商店负责将操作分发到 reducer 中以更新状态。
这里简要介绍一下如何实现所有这些功能。在这个例子中,我通过一个监听器来暴露状态,该监听器会在状态改变时被调用,但你也可以使用 RxJava、LiveData 等来实现。
class BaseStore<S : State>(
initialState: S,
private val reducer: Reducer<S>
) {
private var stateListener: ((S) -> Unit)? = null
private var currentState: S = initialState
set(value) {
field = value
stateListener?.invoke(value)
}
fun dispatch(action: Action) {
currentState = reducer.reduce(action, currentState)
}
fun subscribe(stateListener: ((S) -> Unit)?) {
this.stateListener = stateListener
}
}
这比我们之前的方式更好,因为状态只存在于一个地方(store),并且只能由一个组件(reducer)修改。这样一来,我们不仅可以获得清晰定义的输入和输出,还可以实现单向数据流,正如Esri的这张图表所示:
将其连接到我们的 ViewModel 或 Presenter
请注意图中所示的组件,它负责分发 action。这个组件可以是任何东西,也就是说,你不需要使用 MVVM 来实现这个流程。你也可以创建一个 store 和一个 reducer,然后将它们连接到 presenter 中!
本文中,我们将把它连接到 ViewModel。我们只需要创建一个对 store 的引用,然后像之前修改 state 那样,直接向它分发 action:
class TaskListViewModel(private val repository: TaskRepository) : ViewModel() {
private val store: BaseStore<TaskListState> = BaseStore(
TaskListState.Loading(),
TaskListReducer()
)
// ...
private fun fetchTasks() {
store.dispatch(TaskListAction.TasksLoading)
try {
val tasks = repository.getTasks()
store.dispatch(TaskListAction.TasksLoaded(tasks))
} catch (e: Throwable) {
store.dispatch(TaskListAction.TasksErrored(e))
}
}
}
概要
哇!信息量真大,感谢你看到这里。我们来总结一下刚才看到的所有内容:
MVP 的优势在于它能分离关注点,并使代码全部可用 JUnit 测试。然而,状态管理具有不可预测性,并且轮换机制需要更多的工作来支持。
MVVM 是一种进步,因为它打破了 View 和 Presenter 之间的双向通信,从而实现了更好的旋转支持,但我们仍然存在不可预测的状态管理问题。
MVI 更进一步,通过明确定义的输入和输出,为我们提供了可预测的状态管理。
那我应该用什么呢?
虽然很明显,MVI 是我们讨论过的最强大的模式,但它并非总是适合所有人。如果你的功能非常复杂,用户流程也比较混乱,那么可预测的状态管理就能为你带来诸多好处。
如果你的功能仅仅是获取并显示一些数据,那么MVI的学习曲线以及所需的额外代码可能并不值得付出这些努力。作为开发者,你需要自行决定所需的复杂度。
你完全不必对使用 MVVM 感到愧疚,事实上,在我构建的大多数功能中,它都运行良好。
代码示例
想了解特定模式的实现方式吗?请查看这个GitHub 代码库,其中包含一个示例应用程序,每个架构模式都有一个对应的模块。
如果您有任何疑问,请在下方留言或在Twitter上联系我!如果您对某个特定章节感兴趣,并希望更深入地了解某个特定模式,请告诉我!
文章来源:https://dev.to/adammc331/mvwtf-demystifying-architecture-patterns-ap1




