理解并运用MVC,MVP,MVVM
MVC,MVP and MVVM
MVC
MVC 是一种 GUI 程序架构 模式,其目的是分离关注点,通过将程序按照不同的功能拆分为不同的层级来实现,又称为“分层架构”,具体的做法就是将程序拆分为负责数据存取的“模型”(Model)、负责用户界面的数据展示和响应用户交互“视图”(View)和负责模型和视图间进行传递数据的“控制器”(Controller)三层。
浅谈分层架构
分层架构是指将应用程序按照不同的功能将程序拆分为多个层次,每个层次负责实现自己的功能并开放 API 供其它层次调用其功能。各层按照一定的规则通信,通常是上层依赖下层,下层根据上层输入返回数据,这样单向且顺序的依赖关系。
在 GUI 程序的分层架构设计中,通常分为表示层(Presentation Layer)、应用层(Application Layer)、数据访问层(Data Access Layer)和数据层(Data Layer)。各层次之间的职责和依赖关系如下:
- 表示层
负责用户界面的数据展示和响应用户交互,是程序的入口,即分层中的顶层。
表示层依赖应用层,调用其功能获取数据用于展示或将用户输入的数据保存。 - 应用层
接收表示层传递来的数据,对其进行检验、处理等操作;
应用层依赖数据访问层,调用数据访问层的功能,并将表示层传递来的数据处理后发送给数据访问层,在数据访问层返回数据后,将返回的数据处理后回传给表示层; - 数据访问层
负责的数据的读取与更新等操作。
数据访问层依赖数据层,它将应用层传递来的数据处理后传递给数据层以操作数据,并将数据层返回的操作结果返回给应用层。 - 数据层
负责数据源的维护,包括数据建模、数据存储,一般是和数据库软件、缓存软件、文件系统等负责数据持久化的基础服务交互。
它依赖数据库等外部软件提供的第三方模块来与外部软件实现交互,比如与 MySQL 交互,Java 程序依赖jdbc.jar
,python 程序依赖pymysql
模块,PHP 程序依赖MySQLi
或PDO
扩展, 运行环境为 Node.js 的 JavaScript 程序依赖mysql
模块。
数据层负责维护数据源的软件本就提供数据的读取与关系的功能,如果程序选定好了依赖某个软件提供数据而不再变动,可以合并最后两层为数据访问层,称为三层架构。
但是如果为了程序的拓展性,希望程序可以使用不同的第三方软件提供数据服务,那数据访问层可以为应用层消除不同第三方软件交互方式的差异。具体做法就是抽象数据访问层的 API,不同的第三方软件使用不同的实现,不同实现的接口保持一致。
上述的各层级间的关系,和“队列”这样的线性数据结构中各元素的关系是一样的,又因为每一层都执行特定的任务,所以可以说单向且顺序的分层架构是一种任务队列。
顺序依赖还有一个特点,就是数据传递的过程是固定的,称为单向数据流,很容易追踪数据,方便了程序的调试和测试。
当然了,各个层级之间采用双向依赖、跨层依赖的关系也不是不行,这无非使各层次的依赖关系变得复杂。顺序依赖的关系被打破,数据的传递方向变得多样,数据难以追踪,不便于程序的调试与测试。
MVC 往往不是单向且顺序依赖关系的分层结构
如果视图层到控制器层,再到数据层是单向的依赖关系,视图响应用户输入,转发给控制器,控制器对输入进行处理,转化为对数据模型的操作,再将操作数据模型的结果转化为视图需要的数据回传给视图,视图根据回传的数据作出更新,模型不会主动向视图或者控制器推送消息,这样的 MVC 被称为被动 MVC。
当一个视图表现一个模型数据的时候,这没有问题;但模型数据作为程序内多个视图模块间共享的数据源的时候,一个视图修改了数据数据,其它的视图无从得知,直到其它再次主动读取数据后才知道数据发生了变化。数据发生改变与其它视图主动读取数据获得最新状态两个行为之间存在时间差,这个时间差会导致其它视图展示的数据与数据源之间存在数据不一致的情况。
如果尝试在修改了数据的视图模块中通知其他视图更新,看起来解决了数据不同步的问题,但是如果将来有更多视图,这些视图之间相互影响,每个视图要知道其他所有视图,然后在程序中任何改变了数据状态的逻辑之后都要添加数据同步的逻辑。
如果新增或移除了视图模块,或改动了视图模块的 API,要仔细地翻阅各个视图模块的代码,所有依赖这些模块的其他模块都要更改,一旦遗漏,程序将不能按预期执行。
这样复杂的依赖关系难以维护,应该将这些数据同步的逻辑转移到同一个地方,控制器层和数据层都可以,但是数据层是数据状态发生改变的地方,转移到这里更简单。
这也就意味着模型要直接依赖视图,以实现在数据状态改变时通知视图更新,但视图不应依赖模型,视图与模型之间数据的处理与传递仍由控制器负责。这种模型主动通知视图更新的 MVC 称为主动 MVC。
虽然将数据同步的逻辑转移到了一处,比由视图通知视图的方式更容易维护,但是还是有些麻烦,模型需要知道所有希望响应它的状态的视图,以及通知视图更新的 API,当添加或移除视图后,还要去模型增减相关逻辑代码。
在面向对象编程中,可以使用观察者模式来解决这个问题,定义额外接口,通过实现接口的方式来实现数据同步的逻辑,可以保持模型和视图的相对独立。
模型实现观察者模式中的主题接口毋庸置疑,但 View 与 Controller 都可以是观察者,甚至二者同时作为观察者响应模型的数据变化。
使用观察者模式引入了新的角色,使得 MVC 各层次之间从直接依赖改为了间接依赖,从依赖具体实现改为了依赖抽象。但要实现数据同步,就无法完全消除依赖关系。
谁实现观察者接口决定了 MVC 各层间的依赖关系,如果由 Controller 做观察者或 View 与 Controller 同时做观察者,就成了 MVC 的特定变体 MVP。
MVP
MVP 中 P 是 Presenter,也即 MVC 的 Controller。MVP 指 Presenter 做观察者,负责响应数据变化并更新视图的数据同步逻辑,MVP的特点是 Passive View,被动视图意味着 View 不会改变自身状态,完全由 Presenter 操作,意味着 View 要定义许多更新视图的 API 供 Presenter调用。
MVP 架构中各层通按以下方式通信:
- 当视图接收到用户输入时,转发给 Presenter 进行处理;
- Presenter 根据用户输入对 Model 进行操作,在需要时读取数据或更新数据;
- 当 Model 层数据变动时,可以通知观察者 Presenter;
- 当需要更新视图时 Presenter 通过视图提供的 API 更新视图;
Supervising Controller
监督控制器指 View 与 Controller 同时做观察者,都负责数据同步工作,但是侧重点不同。
将简单的同步操作放在 View 中实现,比如要同步的数据类型一致或者只要做简单的类型转换以及计算;对于涉及到复杂计算的同步操作应在 Prennter 中实现。
这种方式适合有大量的数据同步需求的项目,相比 MVP,Controller 的负担减轻了,但增加了 View 的复杂程度。
MVVM
MVC 和 MVP 本质上是一样的,只是组织代码的方式有差别。而 Model-View-ViewModel(MVVM) 也是将程序分为三个层次,其中数据同步的功能由 ViewModel 完全负责。
相比 MVC,MVVM 引入了新的概念:
- 分离视图
- 声明式视图
- 视图状态
- 数据驱动视图
从业务逻辑中分离视图
视图的功能是显示数据,响应用户输入,它的职责仅此而已。但是它却往往是任务繁重,在业务代码中的占据着不少的比重。我们为视图定义数据,定义响应用户输入的逻辑,本质上是重复劳动。
既然是定义一些东西,完全可以在声明视图结构的时候就一并给定描述。然后专门写一个公用的逻辑,对视图的数据进行绑定,并实现数据的同步功能。
声明的意义在于描述一个事物,不论它是否存在;而定义的意义在于明确一个已经存在的事物。声明是定义的前提,这也是为什么说声明一个变量,不赋值就是空或默认值,而将一个值赋予一个变量被称作定义变量为该值。
声明式视图与视图状态
视图状态指的是视图中可能会变化的数据以及视图响应用户输入的行为。
声明式视图指的是使用特定格式的文本,或者说标记语言来声明视图,在声明中为视图绑定数据源,绑定状态(数据和行为),然后写一个解析器来生成代码。
下面的伪代码解释了这个过程:
<Card data="NameCard"> // 绑定视图数据源
<TextView>{name}</> // 绑定状态(即数据源的属性)
<TextView>{tel}</> // 绑定状态
<Button click="{call}">Call me</Button> // 绑定行为
</Card>
解析器接收声明视图的标记文本和数据源,将不同的标记解析为不同的控件,并使用数据源为其初始化数据,绑定响应用户输入的处理逻辑,实现数据源状态和视图状态之间的数据同步逻辑。
不同的开发平台有不同的实现,比如 Javascript Web 应用使用 HTML 或者 JSX 声明视图,Android 使用 XML 或 Compose 来说明视图,而微软的 WPF .NET 使用 xaml, Blazor 使用 razor。
使用标记语言来声明视图,再通过调用解析器来生成代码的方式定义视图,真正做到了将视图的关注点从业务逻辑中分离。
在定义视图的同时传入数据,又为视图的状态自动绑定了数据,还自动实现了视图与状态间数据同步的逻辑。
解析器最后生成的负责视图和数据同步的代码就是 ViewModel,它在运行后,往往是返回一个与传入数据结构相同的数据对象,通过操作该对象可以获取或更新视图的状态,我们称之为响应式数据。
数据驱动视图
数据驱动视图的直观表现是,改变响应式数据,视图就更新,视图输入改变控件的状态,响应式数据也随之变化。其原理是 ViewModel 维护一个数据集合,与 View 的状态一一对应。本质上还是观察者模式,View 通过观察 ViewModel 的数据变化,来更新视图。而 View 因用户输入产生的状态变化也会主动更新到 ViewModel 的数据集合中。
需要注意的是,ViewModel 名字里的 'Model' 代表的是 View 的状态,是前面提到的响应式数据,它也不简单的是解析器生成 ViewModel 代码时传入的数据,那只代表这个 'Model' 的结构和初始值,View 状态变化,会同步到与该 'Model' 中对应的属性。而 MVVM 中的 Model负责的是数据持久化,它与 ViewModel 中的 Model不一样。
通过更改 ViewModel 中的 Model 的数据就可以更新 View 的显示,就是数据驱动视图。
一切皆数据
数据驱动视图的开发方式已经是所有追求先进性的 GUI 软件开发框架所必备的了。
运行 ViewModel 的代码,其返回值中是不包含 View 的细节,能操作的只有 View 的状态所对应的响应式数据。在开发过程中可以完全当视图不存在,一切都是数据。