最近在实验室内部做了一次分享,主要内容是Android
开发中常使用的三种软件框架模式,MVC
、MVP
和MVVM
,另外写了一个小Demo
来演示这几种模式的实际写法,在此一并发到博客上。
要声明的是,对于软件框架模式,每个人有自己的理解,另外模式也有多种变形,可能会与本文中的不完全一致,因此本文阐述的是我自己对这几个模式的理解。
软件框架模式
什么是软件框架模式
软件框架模式是软件架构的设计理念,一个通用的、可重用的解决方案,用于解决在给定上下文中的软件体系结构问题
软件框架模式即为软件的结构设计,对于一个特定场景下的问题,如何组织和编写软件解决这个问题。就像人类要组织一场活动,如何安排场地和人员以完成活动就是活动的结构设计。
良好的软件框架模式意味着优秀的软件架构,一般具有以下几个优点:
- 框架中各部分模块关系清晰,职责和功能划分明确,功能模块易于复用,降低开发难度
- 每个模块和其他模块之间关联度低,便于单元测试
- 耦合度低,程序灵活,应对新变化如新增需求或者修改需求时便于软件维护
很多人会把软件框架模式和软件设计模式混淆,个人认为,这两者都是软件从业人员经验的总结。但相对而言,软件设计模式更像是编程过程中局部使用的编程技巧,而框架模式则负责整个软件框架,处于全局的视角位置。
常见的软件框架模式
分层模式

「分层模式」是最常见的软件框架模式,很多组织架构和这种模式十分相似。该模式将软件分成若干个水平层,每一层都有特定的角色和职能,代表着应用的某一些功能,不需要知道其他层的细节,层与层之间通过接口通信,OSI的七层和TCP/IP的5层协议都应用了这种模式。
事件总线模式

「事件总线模式」主要用于处理事件,包括4个主要组件:事件源、事件监听器、通道和事件总线。对于不同事件源,产生的事件不同,这些事件都会发布到事件总线上的特定通道上。事件监听器器订阅特定的通道,每当有新事件发生时,订阅了该事件的监听器就可以通过订阅的通道得知该事件,从而做一定处理,每个监听器也可以对多个不同的事件进行监听。事件总线模式和设计模式里的发布订阅模式有一定相似之处。
微核架构

「微核架构」也称为插件化应用模式,由薄核心和多个功能插件组成。这种模式可以通过插件的形式添加额外的特性到核心系统中,提供了很好的扩展性,也使得新特性与核心系统隔离开。
一般来说,核心提供了特定场景下的通用业务逻辑流程,而插件模块则根据这些规则实现具体的业务逻辑,一个例子是FreeSwitch
。FreeSwitch
是一个软电话交换系统,提供了软电话的通用逻辑,如话机两端先进行信令的沟通然后通过这个流程,详细的信令协议则由各插件实现,如SIP协议,H323协议等等,还有语音编解码的具体过程,都是由各语音编码插件完成,FreeSwitch只是提供了整体处理流程。
微服务架构
「微服务架构」提倡将大而全的应用功能拆分成以服务组件为单位的小模块,每个服务组件的服务能力粒度有大有小,小到一个单一的字符串提取,大到一个完整的计费系统。5G
目前使用的就是微服务架构,将运营商的多种功能拆分成小功能单元,每一个新的应用都可以调用不同能力的服务组件以实现自身的功能。通信也在往互联网软件的方向发展。
模型-视图-控制器模式

「模型-视图-控制器模式」也即「MVC
模式」。这个模式多用于GUI
程序的开发,如前端、iOS
和Android
。MVC
模式将软件分成三个层级,模型(Model)、视图(View)和控制层(Controller)。模型包含核心功能和数据,视图展示信息,控制器处理用户输入。这个模式也是本篇文章的主要内容。
MVC模式
MVC模式介绍
MVC
模式主要分为三个部分,Model
、View
和Controller
。
- 模型持有所有的数据、状态和程序逻辑,一个模型可以有不同的多个视图表现形式,复用性高。
- 视图是用户看到并与之交互的页面,通常直接从模型中取得它需要显示的状态与数据,一个视图往往有一个相应的控制器,理论上也可以同不同的模型相关联
- 控制器位于视图和模型中间,负责接受用户的输入,将输入进行解析并反馈给模型
下图是MVC
模式的一个流程示例。View
层有用户点击,Controller
层接收到点击事件,做简单处理之后向Model
层发送请求,Model
层中主要的程序逻辑对接收到的数据进行计算等操作,得到结果后再通知View
层进行视图更新。

以一个计算器为例,如果要用MVC
模式设计软件,则View
层负责设计和绘制界面上的数字和运算符号按钮,Controller
层响应点击事件,处理视图层传递的数据,并将数据传递给Model
层,Model
层通过控制层得到输入的数字和运算符,并对数据进行计算,最终将计算结果传递给View
层进行视图更新。

当用户点击7
*
9
这三个按钮时,Controller
层接收到这三个按钮的点击事件,然后将这三个按钮对应的数字和运算符传递给Model
层进行计算,Model
层计算得到最终结果为63
,随之将63
这个数字传递给View
层进行展示。
这就是MVC
模式在计算器这个软件中的应用。这样分离之后,整个软件耦合度大大降低,软件修改和功能复用方便了很多。如果对计算器界面不满意,那么只需要修改View
重新设计界面;如果想在其他平台如Android
平台复用,Model
包含了主要的程序逻辑,直接将Model
层复用即可,Android
端只需要重写View
层和Controller
层即可,整个软件灵活性大大增强。
在Android开发中的应用
Android
开发其实天生就是MVC
模式,如下图所示:

其中,XML
层作为View
层,定义视图的层级;Activity
作为Controller
层,接收视图的点击等效果,然后将数据请求等发送给Model
层;Model
层负责从网络或者数据库中获取数据。以实现下图的效果为例,界面上有一个按钮,点击按钮显示一条Toast
,具体显示的消息从远端获取。

整个流程过程如下图所示,View
层控制页面布局,Controller
层收到点击事件,处理之后向Model
层获取数据,Model
层获取数据,获取成功通知View
层更新视图。

具体实现中,涉及到通知方式的问题,Android
中有很多方法可以解决这个问题,这里使用接口回调。
首先定义回调接口StringCallback
,里面有一个回调方法。

XML
定义按钮等页面布局。

Activity
实现了回调接口StringCallbcak
,显示Toast
消息,同时监听按钮的点击事件,当按钮被点击的时候,向Model
层获取数据,同时将回调对象传入。

Model
层向远端获取数据,这里为了方便起见,直接使用固定的字符串。数据获取获取成功后,通知View
层更新视图,使用传入的回调对象进行操作。

MVC
模式在Android
中实现大概就是这个框架。不过实现中存在一个问题,Model
层实际应该向View
通知更新视图,但在这里,通知的接受者实际上是Activity
,也就是Controller
。这么做的原因在于,XML
对视图的控制能力是在太弱,只能写一些静态的视图布局,动态的添加和修改等操作还得Activity
完成。因此,这里的框架更像是下图所展示的模式:

这样的话,业务逻辑都放到了Model
层,Model
层已经解耦,但是Controller
层承包了View
层的很多工作,耦合性依然存在。因此,出现了MVC
模式的演化版本,MVP
模式。
MVP模式
MVP模式介绍
与MVC
类似,MVP
模式也主要分为三个部分,Model
、View
和Presenter
。
- 模型持有所有的数据、状态和程序逻辑,一个模型可以有不同的多个视图表现形式,复用性高。
- 视图是用户看到并与之交互的页面,一个视图往往有一个相应的表示层,理论上也可以同不同的模型相关联
- 表示层位于视图和模型中间,负责接受用户的输入,将输入进行解析并反馈给模型
下图是MVC
模式的一个流程示例。View
层有用户点击,Presenter
层接收到点击事件,做简单处理之后向Model
层发送请求,Model
层中主要的程序逻辑对接收到的数据进行计算等操作,得到结果后再通知Presenter
层,随后Presenter
再通知View
层进行视图更新。

以一个计算器为例,如果要用MVP
模式设计软件,则View
层负责设计和绘制界面上的数字和运算符号按钮,Presenter
层响应点击事件,处理视图层传递的数据,并将数据传递给Model
层,Model
层通过控制层得到输入的数字和运算符,并对数据进行计算,最终将计算结果传递给Presenter
层,Presenter
层接收到数据后,再通知View
层进行视图更新。在某种意义上来说,MVP
和代理模式有些类似。

当用户点击7
*
9
这三个按钮时,Presenter
层接收到这三个按钮的点击事件,然后将这三个按钮对应的数字和运算符传递给Model
层进行计算,Model
层计算得到最终结果为63
,随之将63
这个数字传递给Presenter
层,Presenter
层接收到结果,通知View
层进行展示。
这就是MVP
模式在计算器这个软件中的应用,这个模式和MVC
模式最大的区别在于,Model
层和View
层不能直接通信,传递数据等操作都需要Presenter
层介入才能完成。
在Android开发中的应用
以实现和MVC
模式中相同的界面布局为例,界面上有一个按钮,点击按钮显示一条Toast
,具体显示的消息从远端获取。

整个流程过程如下图所示,View
层控制页面布局,Controller
层收到点击事件,处理之后向Model
层获取数据,Model
层获取数据,获取成功通知View
层更新视图。

具体实现中,涉及到通知方式的问题,Android
中有很多方法可以解决这个问题,这里使用接口回调。在MVP
模式的实现中,一共有两次回调用以通知,分别是Model
层通知Presenter
层和Presenter
层通知View
层。因此,一共需要定义两个回调接口,分别由Presenter
层和View
层来实现。
首先定义两个回调接口,里面有两个方法。
 
XML
部分代码与MVC
中一致,在这个实现中,Activity
和XML
共称为View
层,XML
负责静态,Activity
负责动态,同时需要实现View
层接口,并在接收到按钮点击事件时调用Presenter
层的功能。

Presenter
层实现了一个通过Model
层获取数据的方法,同时需要实现Presenter
层接口,供Model
层回调。

Model
层向远端获取数据,这里为了方便起见,直接使用固定的字符串。数据获取获取成功后,通知Presenter
层更新数据,使用传入的回调对象进行操作。Presenter
层接收到通知后,也通过传入的View
层回调对象进行操作。

示例所实现的功能太少,所以现在看来代码略显繁琐,但是代码层次分明,便于扩展。
其实,无论是MVC
还是MVP
模式,都有一个很重要的功能实现是将新的数据(Presenter
或Controller
层)更新到视图(View
层)上。如果有框架能够自动完成这个工作,那么能节省很大一部分工作,开发者可以专注于数据更新和逻辑处理。因此,出现了MVVM
模式。
MVVM模式
MVVM模式介绍
MVVM
模式则是将MVP
模式中的Presenter
改成了ViewModel
层,负责的功能类似,区别在于与View
层的双向绑定。
下图是MVVM
模式的一个流程示例。View
层有用户点击,VM
层通过双向绑定得知,做简单处理之后向Model
层发送请求,Model
层中主要的程序逻辑对接收到的数据进行计算等操作,得到结果后再通知VM
层。VM
数据更改之后,View
层通过双向绑定,从而得知数据更新,并进行视图更新。

MVVM
模式一般很少会自己实现,有现成的框架。前端有Vue.js
,是一个提供了MVVM
模式中数据双向绑定的Javascript
库。Android
方面,Google
提供了Databinding
库。
DataBinding
DataBinding
是谷歌官方发布的一个框架,顾名思义即为数据绑定,是MVVM
模式在Android
上的一种实现,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。
DataBinding
其实并没有实现新的API
来完成双向绑定,只是对原有API
的封装,如findViewById()
和setText()
,亦或是click
事件,这些都被框架隐藏起来了。
至于这些事件的触发,例如View
层接收到点击事件并通知ViewModel
层,则是使用发布订阅模式实现的,如下图:

每当View
层更改时,ViewModel
层通过订阅收到通知;每当ViewModel
层更改时,View
层也会通过订阅收到通知,这样就实现了双向绑定。
这里可能会出现一个循环问题,假设其中某一层A
改变,那么另外一层B
得到通知随之改变,这会导致A
层得到通知,A
层也改变,循环往复出现问题。
DataBinding
通过前后数据对比来决定是否更新,假设View
层需要更新文本信息(如TextView
),则DataBinding
会对比当前的文本和更新的文本,如果不一致则进行更新操作;如果相同,则不做任何操作。其他的控件也有类似的判定操作,这样就解决了数据双向绑定中的循环问题。
在Android开发中的应用
以实现和MVP
模式中相同的界面布局为例,界面上有一个按钮,点击按钮显示一条Toast
,具体显示的消息从远端获取。

整个流程过程如下图所示。

DataBinding
因为有现成的框架,因此这里就不贴具体实现的代码了。
总结
安卓开发中MVC
模式存在的耦合问题在其他开发中可能不存在,各个模式之间也没有什么优劣之分,只有适合与否的问题。整体而言,在不考虑框架的前提下,安卓开发中个人使用MVP
模式较多。