MVC、MVP、MVVM软件架构学习笔记

前言

  MVC、MVP和MVVM是开发GUI(图形用户界面)应用程序时常用的软件架构模式。它们的目的是通过一些手段将GUI应用程序的用户界面(View)和数据(Model)进行分离,提高代码的可复用性和可维护性。本文对三种架构模式的思想进行了深入研究并对比分析了它们的优缺点。

MVC

  MVC模式将GUI应用程序分成三个模块:Model、View和Controller。这三个模块相互之间的交互如图1所示。
MVC交互示意图

图 1 MVC软件架构模式模块之间交互示意图

  对上述三个模块相互之间的交互描述如下: Model可以直接访问数据,其与View和Controller是独立的。Model自身并不关心其是如何被View显示和被Controller操作的,但是Model中数据发生变化后会以一种机制通知View,让View进行更新。为了实现这种机制,可以让相关View在Model上进行注册(或者订阅),当Model数据发生变动时,Model调用View提供的接口可以让对应的View知晓数据的变动。Controller的作用是组织Model和View,控制应用程序的流程。Controller接受来自View反馈的用户行为,然后根据用户行为操作Model引起数据的改变,进而触发View的更新。Controller也可以根据用户行为直接更新View。
  以使用MVC模式开发一个传统的Web页面应用为例,Model、View和Controller的定义如下:
  Model: 用于封装与Web页面应用的业务逻辑相关的数据以及对数据的处理方法。
  View: 浏览器展示给用户的Web页面。
  Controller: 转发请求或者对请求进行处理。
  以JavaScript语言为例,使用MVC模式开发一个加减计数器实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 使用MVC架构模式实现一个加减计数器
* Model实现了对计数器count的封装
* View将count值展示到页面
* Controller处理用户动作
*/
class Model {
constructor() {
this.count = 0;
this.subscribers = [];
}
// 每次加1
addCount() {
this.count++;
}
// 每次减1
subCount() {
this.count--;
}
// 读取count值
getCount() {
return this.count;
}
// 设置count值
setCount(value) {
this.count = value;
}
// 订阅数据变更
subscribe(view) {
this.subscribers.push(view);
}
// 发布通知
publish() {
this.subscribers.forEach(v => v.render(this));
}
}
class View {
constructor(controller) {
this.elText = document.getElementById('#elText');
this.elAdd = document.getElementById('#elAdd');
this.elSub = document.getElementById('#elSub');
this.controller = controller;
// 绑定事件
this.elAdd.addEventListener('click', () => this.controller.handleAdd);
this.elSub.addEventListener('click', () => this.controller.handleSub);
}
render(model) {
this.elText.innerHTML = model.getCount();
}
}
class Controller {
constructor() {
this.view = new View(this);
this.model = new Model();
// 订阅model数据变更
this.model.subscribe(this.view);
}
handleAdd() {
this.model.addCount();
// 发布变更通知
this.model.publish();
}
handleSub() {
this.model.subCount();
// 发布变更通知
this.model.publish();
}
}
// 启动应用
new Controller();

  MVC架构的应用使得复杂的GUI应用程序变得模块化,提高了应用程序的可复用性和可扩展性。MVC架构的应用也使得GUI应用的开发人员变得分工明确,开发人员可以根据自己的专长来从事Model、View或者Controller层面的开发,各个开发人员之间协商好接口之后便可以开始并行开发,从而提高了程序开发效率。虽然MVC架构模式具有上述优点,但是由于其View层和Controller层关联过于紧密,View和Controller一般一一对应,通常将两者打包成一个组件,并且在MVC模式中View层直接可以访问Model,这样可能会造成数据安全性问题。为了让Controller单独可复用以及View和Model进一步隔离,人们对MVC架构模式进行了改进,设计出了MVP这一软件架构模式。

MVP

  MVP模式使用Presenter替换了MVC模式中的Controller,MVP架构模式中各个模块的交互如图2所示。
MVP架构模式模块之间交互示意图

图 2 MVP软件架构模式模块之间交互示意图

  在MVP架构模式中Model和View之间完全独立,Presenter作为中间协调器将两者联系起来。在MVP模式中,Model不知晓View的存在,View也不知晓Model的存在,数据的更新以及页面的刷新全部由Presenter来控制。
  使用MVP架构模式对上述采用MVC架构的加减计数器代码进行重构,重构后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 使用MVP架构模式重构加减计数器
* Model实现了对计数器count的封装
* View将count值展示到页面
* Presenter负责数据的更新与View的刷新
*/
class Model {
constructor() {
this.count = 0;
}
// 每次加1
addCount() {
this.count++;
}
// 每次减1
subCount() {
this.count--;
}
// 读取count值
getCount() {
return this.count;
}
// 设置count值
setCount(value) {
this.count = value;
}
}
class View {
constructor() {
this.elText = document.getElementById('#elText');
this.elAdd = document.getElementById('#elAdd');
this.elSub = document.getElementById('#elSub');
this.presenter = new Presenter(this);
// 绑定事件
this.elAdd.addEventListener('click', () => this.presenter.handleAdd);
this.elSub.addEventListener('click', () => this.presenter.handleSub);
}
render(data) {
this.elText.innerHTML = data.value;
}
}
class Presenter {
constructor(view) {
this.view = view;
this.model = new Model();
}
handleAdd() {
// 更新数据
this.model.addCount();
// 更新页面
this.view.render({
value: this.model.getCount()
});
}
handleSub() {
// 更新数据
this.model.subCount();
// 更新页面
this.view.render({
value: this.model.getCount()
});
}
}
// 启动应用
new View();

  MVP模式与MVC模式相比,其优点在于Model层和View层完全独立,Presenter作为中间层保持View和Model之间的同步。在MVC和MVP模式中均存在一个问题: Controller和Presenter都需要直接或者间接地调用View提供的接口来对页面进行刷新。对于开发者来说,直接调用View接口来更新页面不仅增加了代码量,而且在某些场景下会频繁地操作DOM。如何解决这些问题?在MVP架构模式的基础上,人们引入数据绑定的概念,设计出了目前流行的MVVM软件架构模式。

MVVM

  MVVM模式使用ViewModel替换了MVP模式中的Presenter,该模式下模块之间的交互如图3所示。
MVVM交互示意图

图 3 MVVM软件架构模式模块之间交互示意图

  与Presenter调用View提供的接口进行页面刷新不同,ViewModel通过数据绑定的方式将View和ViewModel进行绑定,当用户操作页面时,ViewModel自动获取到页面的变动,并自动更新Model;当Model数据发生变动时,ViewModel获取到数据变动后自动更新View。数据绑定功能实现了View和Model的自动同步,开发人员尤其是前端工程师在开发GUI应用程序时不用再关心页面如何渲染以及页面渲染是否高效的问题,开发人员重点关注如何处理业务逻辑。
  使用JavaScript语言的MVVM框架Vue.js对上述加减计数器进行重构,重构后的代码如下:

1
2
3
4
5
6
7
8
9
10
/**
* 使用Vue.js重构加减计数器
* Model是一个对象,保存了业务数据,在本例中就是count值
* View可以看做是Vue模板,通过Vue提供的相应的模板语法和ViewModel进行数据和事件的绑定
* ViewModel是一个Vue对象,数据绑定功能由Vue框架内部实现,开发者无需关心,只需关心业务逻辑即可
*/
const model = {
count: 0
}

1
2
3
4
5
6
// View
<div id="app">
<h1>{{count}}</h1>
<button @click="handleAdd">+</button>
<button @click="handleSub">-</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ViewModel
const viewModel = new Vue({
el: '#app',
data: model,
methods: {
handleAdd() {
this.count++;
}
handleSub() {
this.count--;
}
}
})

  MVVM架构模式进一步提高了GUI应用程序的研发效率。为了实现数据绑定,部分MVVM框架可能会创建出很多观察器来对数据进行观察,对于数据结构复杂的GUI应用而言,数据绑定可能会占用大量的内存空间。

总结

  MVC、MVP以及MVVM等MV*架构模式都是为了分离GUI应用程序的数据和界面以提高研发效率和增强GUI应用程序可维护性而提出来的软件架构模式。它们之间的差异在于各个模块之间的耦合程度以及程序研发的关注点。了解不同架构模式的优缺点可以帮助我们在应对不同的业务需求时选择合适的软件架构模式,依靠某一种架构模式应对所有的业务场景是不合适的。

参考文献

  1. https://martinfowler.com/eaaDev/uiArchs.html
  2. https://zh.wikipedia.org/wiki/MVC
  3. https://developer.mozilla.org/en-US/docs/Web/Apps/Fundamentals/Modern_web_app_architecture/MVC_architecture
  4. https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter
  5. https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff649571(v=pandp.10)
  6. https://zh.wikipedia.org/wiki/MVVM
  7. https://blogs.msdn.microsoft.com/johngossman/2006/03/04/advantages-and-disadvantages-of-m-v-vm/
  8. https://docs.microsoft.com/en-us/previous-versions/msp-n-p/hh848246(v=pandp.10)
  9. https://addyosmani.com/blog/understanding-mvvm-a-guide-for-javascript-developers/
  10. https://juejin.im/post/593021272f301e0058273468