网上介绍这几种架构设计的文章很多,不过大多停留在理论层面,实践中具体需要怎么操作,往往比较模糊,而且架构这个东西就像玄学,一千个读者就有一千个哈姆雷特,很难有统一的标准供大家参考,本篇文章将从实践的角度描述一下这几个架构的发展和在移动端的具体的应用
一、架构的发展
1.MVC
MVC 是最早提出的架构模式之一,最早可以追溯到 1970 年代,旨在将用户界面与业务逻辑分离。随着 Web 应用程序的兴起,MVC 模式被广泛应用于 Web 开发中,成为许多 Web 框架(如 Ruby on Rails、ASP.NET MVC 等)的基础
随着MVC中 Massive View/Controller的弊端显现,大量的业务逻辑被规划到了View或者Controller中,导致他们大而且难以维护,难以进行单元测试等
2.MVP
1990年MVP应运而生,Presenter为封装逻辑而生,目标是拆分出View或者Controller中繁重的业务逻辑,让这些逻辑变得具备更高的维护性和可测试性,为什么逻辑在View或者Controller不利于测试,这和大部分应用框架的设计有关系,例如在大家习惯性的思维上,iOS中的Controller的代表是ViewController、Android中是Activity或者Fragment,这些Controller有一个共性特点,他们和View(界面)高度集成,这让单元测试变得困难,因为界面往往是高度依赖整个运行环境的,要测试他们需要大量的Mock工作。
Presenter替代了Controller部分职能,负责管理View和Model的交互,拆出了逻辑部分。注意他并不直接依赖View,进一步解耦了View和Model。这减轻了View和Controller的压力,同时让这些逻辑测试变得容易。但随着应用程序的复杂性指数级上升,庞大的Presenter弊端也开始显现,他太大而且变得难以维护。
3.MVVM
2000年初出现了MVVM,主要是为了支持数据绑定和简化用户界面的开发,它最初是在 WPF(Windows Presentation Foundation)中引入的,MVVM 模式逐渐被许多现代前端框架(如 Angular、Vue.js 和 React)所采用,尤其是在需要双向数据绑定的场景中。
他的一大优势是将界面划分为更小的逻辑单元,MVC、MVP旨在解决业务划分以及模型视图的解耦。而MVVM更进一步,除了逻辑单元变得更小外,他让Model和View的交互变得更容易,为每个View设计了一个专门的视图模型ViewModel来处理View和Model的交互,并承载了相关逻辑。这使得逻辑代码从Presenter拆分到了每个独立的ViewModel,这些ViewModel也保留了Presenter的可测试性。注意,ViewModel和View使用双向绑定,ViewModel不依赖View
二、在Android实际应用中体会一下
从上述的理论介绍上看,我相信很多人看过很多文章,但是发现每个讲的都不太一样,甚至会出现冲突。这不奇怪,回到一开始的讲法上一千个读者就有一千个哈姆雷特。
接下来我们以Android为例,解释一下整个发展过程,相信大家比较容易对号入座,有自己得体会,也就容易区分出这几者的区别了,这比死磕概念会更有价值。
1.MVC在Android中的使用
这里实现了一份基本的伪代码,并在注释中表明了View和Controller可能存在的问题,Massive View/Controller的弊端显现
1)Model 设计
Model包含了数据模型和数据处理(网络IO、本地IO、缓存等),这里简单的定义了一个数据模型User和一个网络数据管理类Model
// Model(数据)
data class User(val name: String, val age: Int)
class Model {
// 网络请求
fun loadData(callback: (User)->Unit) {
callback(User("张三", 18))
}
}
2)View 设计
许多人会将数据模型传入View这里,甚至有人将数据管理器Model控制权也交给View,然后在View中去根据数据控制界面展示。这里的问题是View获得了太大的数据权限,将大量的数据流转逻辑闭环到View内部,让这个View变得的Massive,这种View被称为主动视图。这带来的问题是这些逻辑难以测试,这些逻辑即使通用性很高也难以复用。
// View(视图)
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var nameTextView: TextView
private lateinit var ageTextView: TextView
fun update(user: Model.User) {
// 无法控制大家不在这里写大量的业务逻辑,因为没有一个明确的规定不能在这里写
// [这些逻辑难以进行单元测试,构造View环境比较复杂]
// ...大量业务逻辑...
nameTextView.text = user.name
ageTextView.text = user.age.toString()
}
fun event() {
// 有人为了方便,可能将Model直接记录到这个View,在这里处理大量的数据逻辑
// 导致了View和Model耦合度增加,同时变得难以测试
// [这些逻辑难以进行单元测试,构造View环境比较复杂]
model.requestXXX等()
}
override fun onFinishInflate() {
super.onFinishInflate()
nameTextView = findViewById(R.id.name_text_view)
ageTextView = findViewById(R.id.age_text_view)
}
}
3)Controller 设计
这里和View有着类似的问题,尤其在移动端中,无论iOS还是Android,都将控制器Controller(ViewController/Activity)和View紧密集成在一起,这使得在Controller中做UI操作非常简单容易。为了方便许多人就会将大量的数据逻辑和UI操作都直接写在了Controller中,让这个Controller变得Massive,他和View有同样问题,都难以测试和复用,也造成后续的维护困难。
// Controller(控制器)
class MainActivity : ComponentActivity() {
private lateinit var myView: MyView
private var model = Model()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myView = findViewById(R.id.my_view)
// 无法控制大家不在这里写大量的业务逻辑,因为没有一个明确的规定不能在这里写
// [这些逻辑难以进行单元测试,构造Activity环境太过复杂]
// ...大量业务逻辑...
model.loadData {
// ...大量业务逻辑...
myView.update(it)
// ...大量业务逻辑...
}
}
}
所以MVC模式中,V/C容易边等庞大、复杂,塞了非常多的逻辑,随着迭代的进行、业务复杂的的提升,5000甚至10000行的Controller是非常常见的,他们几乎让后续维护难以进行,单侧也非常困难。同时View和Model也会存在较多的耦合,这个模式只是将系统指责进行了划分,但是并没有一个业界习惯性的标准去让大家遵循。
2. MVP在Android中的使用
吸取了MVC的教训,为了让单元测试覆盖的更高,明确了逻辑应该尽量写到Presenter中,并且Presenter不能直接依赖UI,这样测试才会变得容易
1)Model 设计
Model 同 MVC一样,Model 几乎层没有什么变更
// Model(数据)
data class User(val name: String, val age: Int)
class Model {
// 网络请求
fun loadData(callback: (User)->Unit) {
callback(User("张三", 18))
}
}
2)View 设计
在这个模式下,更强调View和Model的解耦,因此View必须设计的薄,他不能耦合太多的数据逻辑,否则 Presenter 的出现便没有了多大的意义,因此,View的设计应该是尽量轻量的,他不参与过多的逻辑处理,主要负责展示界面,这种View被称为被动视图
// View(视图)
// 这里里边没有什么逻辑,单纯的使用 Presenter 已经处理好的数据,比如age的Int转String操作
// 他里边已经没有什么逻辑了
// 相关逻辑写到Presenter,虽然是一个软性规定,至少开发人员有方向和意识
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), Presenter.MyViewInterface {
lateinit var nameTextView: TextView
lateinit var ageTextView: TextView
override fun updateName(name: String) {
nameTextView.text = name
}
override fun updateAge(ageStr: String) {
nameTextView.text = ageStr
}
override fun onFinishInflate() {
super.onFinishInflate()
nameTextView = findViewById(R.id.name_text_view)
ageTextView = findViewById(R.id.age_text_view)
}
}
3)Presenter 设计
Presenter 负责协调View和Model,但不能直接依赖View,因为UI会使得测试变得困难,所以对View的操作应该通过接口ViewInterface来代理,以解除对View的直接依赖。此外他承载大量的逻辑,将之前Controller的指责都承接过来,由于和UI解耦了,使得这些逻辑变得容易测试。虽然测试性问题解决了,但是随着时间推移,他也会逐渐的变得Massive,后续也难以维护。
// Presenter 这里不依赖界面相关环境,单元测试变得容易
// 1.负责View和Model的交互,不能直接依赖View
// 2.相关逻辑写到这里,虽然是一个软性规定,至少开发人员有方向和意识
class Presenter(private val view: MyViewInterface) {
interface MyViewInterface {
fun updateName(name: String)
fun updateAge(age: String)
}
private val model = Model()
fun refreshData() {
// [这些逻辑容易进行单元测试]
// ...大量业务逻辑...
model.loadData {
// ...大量业务逻辑...
view.updateName(it.name)
// ...大量业务逻辑...
view.updateName(it.age.toString())
}
}
}
4)Activity 环境
Activity只作为基本的环境布置和生命周期分发,他里边几乎不参杂具体业务逻辑,这让生命周期管理等操作更容易维护,排查内存相关问题等也减轻了负担。
// 这里仅仅负责环境相关的初始化和生命周期相关操作
// 相关逻辑写到Presenter,虽然是一个软性规定,至少开发人员有方向和意识
class MainActivity : ComponentActivity() {
private lateinit var myView: MyView
private lateinit var model: Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myView = findViewById(R.id.my_view)
model = Presenter(myView)
model.refreshData() // 这里只触发事件,业务逻辑交给Presenter
}
}
相比于之前的MVC,MVP的最大改变就是指责相对更加清晰,极大的解耦和View和Model,开发人员更容易形成逻辑封装到 Presenter 的意识。不过仍然没有一个能在框架层面做出相对较强的标准去参考,如果开发人员没有较好的封装意识,会将所有业务逻辑都往一个 Presenter 去写,它虽然解决了可测试性问题,但没解决随着迭代而变得庞大的问题。
3. MVVM在Android中的使用
MVVM中,强调View和Model解耦的同时,也强调模块划分,即避免Massive Presenter。所以我们的业务应该按照模块将View划分隔开,让每一个View都有一个对应的模型视图ViewModel,这个ViewModel负责相关控制协调逻辑,并将后台数据转化为View能够理解的方式。对于View和Model的交互使用双向绑定,让View变成真正的被动视图,极大程度解耦的同时完全由数据驱动。这种设计中相当于吧以前的一个大的View划分为多个小的View,把以前一个大的 Presenter 划分为多个ViewModel。在框架层面限制了开发思维,由于模块限制和划分,模块之前的耦合度变低,这不得不逼迫开发者写出更加内聚的逻辑。
1)Model 设计
这几种架构模式中,Model其实都可以是一样的,本质上都是对数据获取、处理等操作
// Model(数据)
data class User(val name: String, val age: Int)
class Model {
// 网络请求
fun loadData(callback: (User)->Unit) {
callback(User("张三", 18))
}
// 编辑用户信息
fun updateData(userName: String, age: Int, callback: (User)->Unit) {
callback(User(userName, age))
}
}
1)View 设计
由于强调View的划分,为了表示出这个行为,这里我们设计两个视图,假定当前界面有两个大区域模块,分别是MyView和MyEditUserView,一个是展示用户信息区域,另一个是编辑用户信息区域,他们之前的逻辑是完全可以分离开的
1)MyView 设计
这里负责展示用户信息的区域视图,他只负责监听 MyViewModel 的数据变更,当数据变更是他即使将新数据刷到自身上,改变展示(这里被动绑定);同时他持有ViewModel,可以根据事件要求ViewModel做出数据变更(这里是主动绑定)。这两个绑定实现了双向绑定,视图和模型通过ViewModel(展示层)做出解耦
// View(视图1:展示用户信息)
// 这里里边没有什么逻辑,单纯的使用 MyViewVM 已经处理好的数据,比如age的Int转String操作
// 相关逻辑写到MyViewVM,虽然是一个软性规定,至少开发人员有方向和意识
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var nameTextView: TextView
private lateinit var ageTextView: TextView
private lateinit var vm: MyViewModel
fun bindViewModel(vm: MyViewModel) {
this.vm = vm
// 绑定VM的,监听VM中的数据变更(绑定1:监听)
DataBinding.bind(nameTextView, vm.username)
DataBinding.bind(ageTextView, vm.userAge)
}
fun event() {
// 通知VM变更(绑定2:通知)
vm.requestXXX()
}
override fun onFinishInflate() {
super.onFinishInflate()
nameTextView = findViewById(R.id.name_text_view)
ageTextView = findViewById(R.id.age_text_view)
}
}
2)MyViewModel 设计
MyViewModel负责对接MyView相关的数据处理逻辑,它将根据视图的展示需求定制可见听的成面变量给到View做监听绑定(Android中可以用LiveData),同时负责这个View相关的逻辑处理,与Model对接等相关操作。这个ViewModel负责闭环这个View相关的逻辑,他不能直接依赖View,以保证单元测试的顺利进行
// ViewModel(视图模型,处理对应视图的相关逻辑)
// 视图模型1:对应MyView,管理MyView相关数据和逻辑
// 将后台的数据处理后,转化为方便View直接展示的形式,负责View和Model的交互
// [他不直接依赖View,非常容易进行单元测试]
class MyViewModel() : ViewModel() {
// 为了代码简短,这里直接公开 MutableLiveData,实际中国最好是只公开LiveData
val username = MutableLiveData<String>()
val userAge = MutableLiveData<String>()
private val model = Model()
fun refresh() {
// [这些逻辑容易进行单元测试]
// ...大量业务逻辑...
model.loadData {
// ...大量业务逻辑...
username.value = it.name
// ...大量业务逻辑...
userAge.value = it.age.toString()
}
}
}
3)MyEditUserView 设计
对于页面的其他视图区域,我们进行视图的划分,这是另一个区域的视图,设计理念同MyView
// 很多时候,View的逻辑是需要拆分的更小的,一个视图和视图模型往往不够,需要更多的View和ViewModel
// View(视图2:编辑用户信息)
class MyEditUserView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var vm: MyEditUserViewModel
fun bindViewModel(vm: MyEditUserViewModel) {
this.vm = vm
// 类似MyView的绑定方案,处理当前视图的独立逻辑
}
fun event() {
// 开始更新用户信息
vm.update("李四", 30) {
// 更新结果处理
}
}
// ...
}
4)MyEditUserViewModel 设计
这是针对MyEditUserView视图区域设计的专有模型视图,他负责MyEditUserView相关的处理逻辑,设计标准同MyViewModel
// 视图模型2:对应MyEditUserView,管理MyEditUserView相关数据和逻辑
// 将后台的数据处理后,转化为方便View直接展示的形式,负责View和Model的交互
// [他不直接依赖View,非常容易进行单元测试]
class MyEditUserViewModel() : ViewModel() {
private val model = Model()
val updateUsername = MutableLiveData<String>()
val updateUserAge = MutableLiveData<Int>()
fun update(userName: String, userAge: Int, callback: (Model.User) -> Unit) {
model.updateData(userName, userAge, callback)
}
}
5)Module 模块划分
实际开发中,我们需要提供框架能力进行模块划分,相当于模块入口,负责每个模块的环境配置、生命周期派发等。同时模块划分的框架思路实现也给后续开发人员提供了参考思路,限制了模块之间高度耦合的发生,我们这里假定设计两个模块,用Module表示,实际开发中这个框架设计也是另外一个话题了,这里只伪代码
// 用户信息展示模块,负责环境和生命周期
class Module1(private val activity: MainActivity) { // 模块1
private lateinit var myView: MyView
private lateinit var myViewVM: MyViewModel
fun onCreate() {
myView = activity.findViewById(R.id.my_view)
myViewVM = MyViewModel()
myViewVM.refresh()
}
}
// 用户信息编辑模块,负责环境和生命周期
class Module2(activity: MainActivity) { // 模块2
private lateinit var myEditUserView: MyEditUserView
private lateinit var myEditUserVM: MyEditUserViewModel
fun onCreate() {
myEditUserView = activity.findViewById(R.id.my_edit_user_view)
myEditUserVM = MyEditUserViewModel()
}
}
6)Activity 环境
Activity同样只作为基本的环境布置和生命周期分发,他里边几乎不参杂具体业务逻辑。
// Controller(控制器),这里仅仅负责环境相关的初始化和生命周期相关操作 --------------
// 实际开发中,这里边业务逻辑还会非常复杂,可以继续对界面进行模块抽象,模块内部再进一步划分视图区域
// 相关逻辑写到ViewModel,虽然是一个软性规定,至少开发人员有方向和意识
class MainActivity : ComponentActivity() {
private val module1 = Module1(this)
private val module2 = Module2(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
module1.onCreate()
module1.onCreate()
}
}
这里可以看出,MVVM和MVP主要升级点是Presenter的逻辑拆分到摸一个更小的ViewModel中,这让可持续性维护可能性变得更高,View和ViewModel的双向绑定并没有破坏逻辑的可测试性,开发人员更容易写出薄的View,由于ViewModel相对较小,他复用也变成了可能。实际操作中ViewModel复用是非常常见的,由于架构限定,开发人员组织代码更加趋同,不会因为水平差异写出差异非常大的代码
三、总结
通过上边的层层推演,发现架构演变主题的都在围绕可维护性、可测试性、封装这几个主题,其中这些架构要解决的主要问题就是,MVC主要实现了系统模块指责划分、MVP进一步提高了可测试性、MVVM更进一步提高了可维护性,不过一个架构要承担更多的能力和特性就需要更多的角色和对象加入,例如从我们上诉案例中可以看出,MVP增加了Presenter对象,MVVM增加了Module、ViewModel、双向绑定等逻辑,这些增加的对象在一定程度上增加了系统复杂度。
具体来说
- MVC
实现了系统模块指责划分,但 Controller 承载了大量的业务逻辑,不利于维护和测试 - MVP 中 Presenter 拆除
解耦视图和模型,提高了可测试性,但是同样承载了大量逻辑 - MVVM 中 ViewModel 经过
模块拆分了逻辑,提高了可测试性、维护性、复用等能力,但应用复杂度增加了
开发中具备一定复杂度或体量或是人员规模的项目是需要MVVM才能更好的去组织代码的,他能在一定程度上去规范程序员编码行为的。但如果是个人项目或者小微项目,MVVM可能会给开发过程带来数据绑定、模块通信等多余的设计,这种项目或许考虑MVC、MVP方案能带来更好的开发体验。
当然这些架构并不是互斥的,他们可以迭代的过程,就如我上边案例中的过程一样,我们一开始可以是MVC,然后逐渐迭代为MVP甚至MVVM都是完全可行的。
虽然在一个经验老到的写手上并不限于什么架构,都能组织好代码。但架构往往为了抹平能力差异而必须存在的。架构设计见仁见智,同样都可以说是MVVM,但是设计出来可能完全不是一套开发思路。不过一切架构的最终目标都要考虑一个点,框架层面能限制程序员的肆意行为