MVC&&MVP&&MVVM
MVC && MVP && MVVM
[TOC]
概念
架构模式
也叫架构风格,一个架构模式描述软件系统里的基本的结构组织或纲要。架构模式提供一些呈先定义好的子系统,指定它们的责任,并给出把它们组织在一起的法则和指南。一个架构模式常常可以分解成很多个设计模式的联合使用。MVC模式就属于架构模式。[7]
关键词
-
纲要
-
预先定义好的子系统
-
指定子系统责任(职责边界)
-
组织的法则和指南(协作的方式)
-
设计模式的联合使用
业务逻辑[8]
业务逻辑是业务领域的逻辑,是提供业务服务的规则和流程
业务逻辑包括
- 领域实体
- 业务规则
- 完整性约束
- 业务流程
应用逻辑
应用逻辑是指视图和业务协作的一套机制
应用逻辑包括
- 连接视图和业务
- 串联业务
- 业务切换
- 业务管理
关注点分离
在讨论这几种架构模式之前,我们先讨论一个关注点分离的设计原则。
关注点分离 (Separation of concerns,SOC)是对只与「特定概念、目标」(关注点)相关联的软件组成部分进行「标识、 封装和操纵」的能力,即标识、封装和操纵关注点的能力。 是处理复杂性的一个原则。 由于关注点混杂在一起会导致复杂性大大增加,所以能够把不同的关注点分离开来,分别处理就是处理复杂性的一个原则,一种方法。[1]
从关注点分离的定义我们可以关注到有四个关键词:
-
特定概念、目标
-
标识
-
封装
-
操纵
从分离点关注的概念和关键词描述,可以映射到我们日常的一些软件设计方法,例如:
- 职责分离
- 4+1视图
- 架构风格(架构模式)
- 外观模式(从封装性关键词联想)
分层设计这种架构风格是关注点分离的一种实施例子,因此分层设计满足关注点分离原则,那我们对应上面说的四个词来阐述对于这几种架构模式的理解,以及如何影响软件的质量。
MVC
MVC 关注点分离出三个要素
- Model(模型)
- View(视图)
- Controller(控制器)
下图是组件之间的交互[3]
MVC协作 figure [1]
职责说明
基于上图的交互,我们对MVC各个组件的职责进行一下说明
- Model
- View
- Controller
在说职责之前,我们先想一下如何划分职责,可以达到职责单一,并且可以提高软件的复用性和可维护性。
View
-
提供信息的展现界面
对于同一份数据,可以通过不同的视图显示不同的效果
-
接收用户的操作指令
在这里我想说下win32程序窗口创建的代码[4]
步骤:
- 声明和填充WNDCLASSEX对象
- 调用RegisterClassEx注册声明的窗口类型
- 调用CreateWindow创建注册的窗口类型
- 调用ShowWindow显示窗口
- 创建消息循环,派发消息
- 在WndProc回调函数处理通知的窗口消息(如下,基本的业务逻辑入口都在这个函数里面)
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
TCHAR greeting[] = _T("Hello, Windows desktop!");
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// Here your application is laid out.
// For this introduction, we just print out "Hello, Windows desktop!"
// in the top left corner.
TextOut(hdc,
5, 5,
greeting, _tcslen(greeting));
// End application specific layout section.
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
break;
}
return 0;
}
该代码是一个回调方法,用户提供给系统通知窗口相关的事件,这个和Android设置一个回调类似,不同的事,窗口事件包括了用户的指令(点击)和窗口本身的内部事件(绘制,销毁等)
如果不做任何代码的优化,在一个复杂的用户界面,这个函数会变的异常庞大,考虑到很多的业务逻辑是可以复用的,因此会提取业务相关的指令汇集到一个集中的处理。
这样的话,WndProc会出现一些变化,那假设我们理解的MVC各个组件职责如下图
WndProc职责抽离 Figure 2
Controller && Model
根据软件架构设计的分层[5]介绍的四层模型,核心是问题领域层,问题领域层对应业务实体层和业务逻辑层。
分层模型 Figure 3
按照分层设计,我们假设Controller对应问题领域层,那么Controller将变成整个软件的核心层,Controller内部将包含业务实体和业务逻辑。
按照这个定位,那么Model就会成为职责单一的数据管理层,那么不同的 Model可以对不同的数据源进行管理,
例如,数据库数据源,本地文件数据源,网络数据源,以及不同源的组合源
被动MVC && 主动MVC
如果Model不承担通知视图更新的职责,由Controller负责通知更新,那么就是被动MVC模式
如果由Model承担通知视图更新的职责,是主动MVC模式
从实际操作,我倾向于被动MVC模式,这样可以提升View渲染的性能,而不需要经过Model层操作之后进行View渲染。
在实现中,需要注意数据一致性的问题。
例如信息流场景,回看历史数据的时候,如果之前Model持久出现异常,没有一直合适的机制进行处理,则会导致中间丢失部分数据,前后浏览的记录会不一致。
效果
按照上面的设计,会带来一些设计的效果
- Controller成为软件的核心层,Controller可以通过组合View和Model,提供软件的灵活性和可配置性
对figure [1]中的协作关系从软件设计上反映
MVC结构 Figure 4[被动MVC模式]
- Controller是一个策略,通过配置不同的Controller,对View的请求定制不同的响应
- View是一个观察者策略,和Controller进行解耦
- Controller也可以配置不同的Model,使用不同的数据管理
- 不同层有不同的数据抽象,使用不同的数据模型
- View不能直接访问Model,这样会破坏组件间的相互独立
MVC 本质上是通过分层的思想,使Controller,View,Model三个组件相互独立,支持View视图的灵活变化,以及Controller的配置改变数据层和业务规则。
Controller [假设]
业务逻辑层,负责业务逻辑的处理
Model [假设]
数据管理层,负责数据的获取
问题
按照上面的分析,以及各个组件的职责,的确可以达到重用Model,View的目的,同时Controller可以对不同的数据源,视图进行配置,灵活的满足客户需求。
但是这样Controller会显得过于庞大,我们实际只分离出了数据管理层,并没有对WndProc中各种业务处理进行分离,那是否我们可以配置多个Controller对应不同的M,这样Controller的就变成更简洁,职能更单一。
答案是应该这么做,并且可以提高Controller的复用性。
但是我们刚刚说到应用逻辑,监督和协调整个应用的活动。
如果Controller管理View以及Model之间的协作关系,同时Controller又具有业务的功能,那么对于Controller的职责来说是不单一的,因此我们需要重新定位Controller和Model的职责。
MVC职责修正 Figure 2 [修正]
那承担数据管理职责的层,对应数据的持久化的工作,则属于基础设施层,Model应该是包括业务逻辑的要素(领域实体,规则,约束),业务服务提供业务流程的协作管理。
MVC_修正 Figure 3[修正]
Controller
应用逻辑组件
连接视图和业务,控制程序的流程
Model
业务逻辑组件
用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法
Model层涉及的业务逻辑元素较多,其内部可以分离出数据访问层,用于抽象业务数据的获取,包括本地数据和远程数据的获取,目标是屏蔽掉数据访问的细节,例如数据库的连接,数据的存储过程细节,服务器接口协议以及解析过程。
核心
- 关注点分离思想
- 组件之间相互独立
MVP
通过MVC的分析,如果按照**Figure 4 **的交互关系,本质上和MVP是没有区别的只是把
Controller -> Presenter
Presenter的职责和Controller职责是完全一样的。
MVP是MVVM实施的一种特例。
MVP结构 Figure 5
MVC&& MVP区别
说到这里回到MVC 效果第5点
通常的谈到MVC和MVP的区别的时候,能否从View直接访问Model是一个区别的特征。
但是这个不是MVC必须的,很大的原因是因为MVC架构的框架实现导致这种理解。
因此我认为讨论View是否直接访问Model的优缺点更有意义。
View是否直接访问Model优缺点
-
优点
- View直接访问Model,缩短了链路的路由长度,在某些场景,会某些场景具有性能优势
- View直接访问Model,不需要实现一套从View->Controller->Model->Controller->View的数据更新机制,实现比较简单,对于开发人员的要求较低,开发成本较低
-
缺点
-
View直接依赖Model,虽然可以通过接口接口具体的Model类,从上下文多了一个外部的交互组件,
关联的外部系统越多,就越容易造成自己系统的复杂性
-
View直接依赖Model,意味着View会直接和数据管理层发生联系,那意味着View将包含一部分业务逻辑,这对View的职责单一性产生了破坏,也导致了那部分业务逻辑无法重用
-
对于Model来说,越多的外部调用者,破坏了代码的统一管理,在问题排查时增加了Model的维护成本
-
职责
View
职责上和MVC的View没有区别,对比MVC的View,没有给出明确的实现规范,MVP比较明确的提出了接口方式的抽象
Presenter
负责应用逻辑处理,和对视图的更新
Model
提供业务逻辑服务
Model需要提供对Presenter的数据更新没有具体的实现要求,可以通过回调,观察者去实现,但是Model需要实现相应的回传机制,把值变化返回给到Presenter,对应Model来说,Presenter是解耦的,保持Model的独立性
优点
- 从架构思想上对比MVC,明确了视图层,业务层,数据层三大组件的通信方式,隔离了View和Model
- 明确了通过View的接口抽象,实现Presenter的复用和View的独立性
- 基于接口方式的代码,易于测试和方便调试,也易于理解
缺点
- View的接口抽象,需要定义View的接口类,这样会引起View接口类和View具体类的一个耦合,具体体现就是视图发生变化,接口类需要修改,同时Presenter内也需要进行修改。
核心
- View和Model隔离
- View视图的抽象隔离
MVVM
介绍MVVM之前,我们先介绍一下
双向绑定
双向绑定是指视图和模型之前的动态互动关系,当模型发生变化时,视图能更新;同时当视图发生变化时候,模型也随之更新,这种双向的互动机制,就是双向绑定。
MVVM结构 Figure 6
职责
MVVM本质上和MVP也没有本质的区别,但是View和ViewModel之间使用了双向绑定,其实是为了解决MVP模式中需要手动同步Model->Presenter->View,View->Presenter->Model的手动代码编写,在MVVM架构风格提供的框架中自动的完成了。
View
对比MVP方式,通过视图绑定解耦了View的具体实现,同时通过数据的统一抽象框架,不需要再单独去编写View视图的对外接口,减少了开发的工作量。
同时基于数据抽象的方式,降低了业务对视图的敏感性。
ViewModel
提供了双向绑定的实现机制提供应用逻辑服务
Model
提供业务逻辑服务,Model通过可被观察的数据,直接实现了Model到ViewModel的数据返回,这样可以不用在Model内部维护一套数据更新的机制,通过统一的可观察的数据对象,达到数据的更新通知。
也就是交互图的Model->ViewModel的notify操作,不是必须的,如果使用可观察的对象,对可观察对象的修改,自动就会传递到视图层
优点
- 改进了MVP需要抽象View的接口方式,而采用了数据抽象的方式,View的功能更加内聚
- 同时采用数据绑定框架,可以避免MVP数据变化在各层顺序传递的流转,可以直接从业务传递到视图,代码看起来更简洁
- 因为基于数据抽象,ViewModel没有直接和View产生接口调用的关系,而数据抽象是基于问题域的抽象,相对稳定很多,即使视图发生了很大的变化,ViewModel涉及的修改也很少甚至不修改,因此ViewModel的复用性和维护性更高
- 基于上一点的优势,ViewModel很适合进行对不同的Model进行适配,而不需要修改View和Model,组件的独立性更好
缺点
-
MVVM方式实现比较复杂,实现上有比较高的开发成本,基本上需要利用第三方框架(例如data-binding框架),而第三方的框架的使用也具有一定的学习成本
-
MVVM通过更抽象的数据接口实现组件间的通信,这种方式的代码不利于代码和问题的跟踪排查
核心
- 数据绑定,数据驱动,视图完全隔离
- 可观察的数据对象
- 视图直接绑定ViewModel中的值对象,当值发生变化时候,视图可以自动更新,同时支持双向数据绑定,视图变化修改ViewModel中的值对象
可参考官方提供的指导例子应用架构指南
总结
MVC,MVP,MVVM都是在MV*模式上的改进,本质上是一样,只是在组织的法则和指南上,即协作方式进行了不同的设计,以适用不同的场景,和减少开发人员的工作量。
同时也有相应的优缺点,新的改良都必须满足之前模式的核心内容。
因为本质上一样,基于场景来说,只能说通常开发使用,基于大部分人对MV*的理解来说
- MVC适用了功能较少,业务逻辑比较简单的小项目
- MVP考虑到接口化的交互方式,使用了业务比较复杂,但是界面比较简单的项目
- MVVM适用于业务复杂,界面复杂的项目,通过数据驱动,消除了接口设计繁复的问题
共性
- 明确模块的职责以及定义职责间的界限,可以通过[2]提供的指导方法进行思考
- MVP明确了View和Model的隔离,本质上是更加明确了职责和职责间的边界,是高复用的基础
- 识别影响软件质量中的因素(可复用性,可扩展性)
- 视图的变化比模型的变化更加频繁,分离Model可以提升Model的复用性
- 关注内聚的核心能力以及职责单一性,天然的能提升模块的可扩展性
- 关注架构模式背后映射的设计模式,以及软件设计的一些方法原则(例如OOP原则),如何实现高内聚,低耦合,高复用的原理
- 理解架构思想,并在实现上通过框架以及工具的手段去保证实施
- Android的数据绑定的框架实现,以及视图生命周期的自动管理
- 视图更新的实施过程中都会涉及到线程的问题,这块需要在业务层进行设计
不能解决的问题
上面说了使用架构模式通过关注点分离,实现了不同层的关注,但是架构模式本身解决不了子系统内部的问题,例如业务之间的抽象。
参考
[1] https://zh.wikipedia.org/zh/%E5%85%B3%E6%B3%A8%E7%82%B9%E5%88%86%E7%A6%BB 关注点分离
[2]http://aspiringcraftsman.com/2008/01/03/art-of-separation-of-concerns/ 关注点分离的艺术
[3]https://zh.wikipedia.org/wiki/MVC MVC模式
[4]https://docs.microsoft.com/zh-cn/cpp/windows/walkthrough-creating-windows-desktop-applications-cpp?view=vs-2019 演练:创建传统的 Windows 桌面应用程序
[5]《软件架构设计》第二版 温昱 第13章《如何分层》
[6]https://developer.android.com/jetpack/docs/guide 应用架构指南
[7] https://baike.baidu.com/item/架构模式/9283553?fr=aladdin 架构模式