目前已转至个人博客,本系列地址:Lam's Blog - Knowledge as Action
前言
本文基于周鸿博的文章Android App的设计架构:MVC,MVP,MVVM与架构经验谈,同时结合网上其余与MVC,MVP,MVVM相关的文章。
关于重构的一些话
该文章可以作为项目初期架构选型时的参考,也可作为项目开展一些阶段之后进行重构的参考,所以首先扯点题外话:为什么我们需要重构?
- 重构改进软件设计
只为了短期目的或者在完全理解整体设计之前编写出来的代码,会导致程序逐渐失去自己的结构。这时如果没有重构,程序的设计会逐渐腐败变质,程序员愈来愈难通过阅读源码而理解原本设计。重构很像是在整理代码,你所做的就是让所有东西回到应该的位置上。代码结构的流失是累积性的。愈难看出代码所代表的设计意涵,就愈难保护其中设计,于是该设计就腐败得愈快。经常性的重构可以帮助代码维持自己该有的形态。 - 重构使软件更容易理解
所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机做什么事,它的响应则是精确按照你的指示行动。你得及时填补“想要它做什么”和“告诉它做什么”之间的缝隙。这种编程模式的核心就是“准确说出我所要的”。除了计算机外,你的源码还有其他读者:几个月之后可能会有另一位程序员尝试读懂你的代码并做一些修改。我们很容易忘记这第二位读者,但他才是最重要的。计算机是否多花了几个小时来编译,又有什么关系呢?如果一个程序员花费一周时间来修改某段代码,那才关系重大—如果他理解你的代码,这个修改原本只需一小时。 - 重构帮助找到bug
对代码的理解,可以帮助我们找到bug。有些人只要盯着一大段代码就可以找出里面的bug,但大多数人不行。如果对代码进行重构,我们就可以深入理解代码的作为,并恰到好处地把新的理解反馈回去。搞清楚程序结构的同时,也清楚了自己所做的一些假设,从而更加容易把bug揪出来。 - 重构提高编程速度
良好的设计是快速开发的根本──事实上,拥有良好设计才可能做到快速开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,难以或者甚至无法添加新功能,修改时间愈来愈长,扩展性与可维护性越来越差。因为你必须花愈来愈多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补丁,新特性需要更多代码才能实现,最终变成一个恶性循环。 - 重构使单元测试成为可能
Android作为对单元测试最不友好的系统环境,大多数开发者在刚开始接触时都会直观地把和当前界面有关的所有逻辑都放在Activity或者Fragment等视图里面,造成View层与逻辑层甚至数据层深度耦合,而单元测试对View的测试非常乏力,也不值得花大量时间在这上面,同时因为逻辑的过度耦合,每一个类里面有非常多的私有依赖无法进行mock,从而无法达到尽可能全面测试的目的。但是单元测试可以测出整个测试流程中80%的bug,这是对代码质量的一个重大保障。 - 重构应该是开发人员的必备技术手段
一名开发人员的良好发展路线应该是由会写代码转为写好代码,也是从一名实现者到架构者的转变,甚至即使是单纯的实现者,也应该追求自己能写出越来越优雅和高效的代码。业务需求与技术需求不应该是对立面,技术需求最终也是服务于业务需求,更好的代码结构才能帮助项目更快速地开发。
框架介绍与选择
MVC
MVC简介
MVC全名是Model View Controller,如图,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。
其中M层处理数据,业务逻辑等;V层处理界面的显示结果;C层起到桥梁的作用,来控制V层和M层通信以此来达到分离视图显示和业务逻辑层。
Android中的MVC
Android中界面部分也采用了当前比较流行的MVC框架,在Android中
- 视图层(View):
一般采用XML文件进行界面的描述,这些XML可以理解为Android App的View。使用的时候可以非常方便的引入。同时便于后期界面的修改。逻辑中与界面对应的id不变化则代码不用修改,大大增强了代码的可维护性 * - 控制层(Controller):
Android的控制层的重任通常落在了众多的Activity的肩上。这句话也就暗含了不要在Activity中写逻辑代码,要通过Activity交割给Model业务逻辑层处理,这样做的另外一个原因是Android中的Actiivity的响应时间是5s,如果耗时的操作放在这里,程序就很容易被回收掉。 - 模型层(Model)
我们针对业务模型,建立的数据结构和相关的类,就可以理解为AndroidApp的Model,Model是与View无关,而与业务相关的。对数据库的操作、对网络等的操作都应该在Model里面处理,当然对业务计算等操作也是必须放在的该层的。 - 存在的问题
xml作为view层,控制能力实在太弱了,你想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,这些都没办法在xml中做,只能把代码写在activity中,造成了activity既是controller层,又是view层的这样一个窘境。在Android开发中,Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受和处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿。
Android上的MVC模式,view层和model层是相互可知的,这意味着两层之间存在耦合,耦合对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。
MVP
MVP简介
MVP从更早的MVC框架演变过来,与MVC有一定的相似性:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。
MVP框架由3部分组成:View负责显示,Presenter负责逻辑处理,Model提供数据。在MVP模式里通常包含3个要素(加上View interface是4个):
- view:负责绘制UI元素、与用户进行交互(在Android中体现为Activity等)
- Model:负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合)
- Presenter:作为View与Model交互的中间纽带,处理与用户交互的业务逻辑。
- *View interface:需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试
- Tips:*View interface的必要性
回想一下你在开发Android应用时是如何对代码逻辑进行单元测试的?是否每次都要将应用部署到Android模拟器或真机上,然后通过模拟用户操作进行测试?然而由于Android平台的特性,每次部署都耗费了大量的时间,这直接导致开发效率的降低。而在MVP模式中,处理复杂逻辑的Presenter是通过interface与View(Activity)进行交互的,这说明我们可以通过自定义类实现这个interface来模拟Activity的行为对Presenter进行单元测试,省去了大量的部署及测试的时间。
MVP的优点
- 在MVP中,Activity的代码不臃肿;
- Model与视图完全分离,我们可以修改视图而不影响Model
- 可以更高效地使用Model,因为所有的交互都发生在一个地方——Presenter内部
- 我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比Model的变化频繁
- 如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)
- 具体到Android App中,一般可以将App根据程序的结构进行纵向划分,根据MVP可以将App分别为模型层(M),UI层(V)和逻辑层(P)。
- UI层一般包括Activity,Fragment,Adapter等直接和UI相关的类,UI层的Activity在启动之后实例化相应的Presenter,App的控制权后移,由UI转移到Presenter,两者之间的通信通过BroadCast、Handler或者接口完成,只传递事件和结果。
- 举个简单的例子,UI层通知逻辑层(Presenter)用户点击了一个Button,逻辑层(Presenter)自己决定应该用什么行为进行响应,该找哪个模型(Model)去做这件事,最后逻辑层(Presenter)将完成的结果更新到UI层。
MVP模式的一些弊端:
- Presenter(以下简称P)层与View(以下简称V)层是通过接口进行交互的,接口粒度不好控制。粒度太小,就会存在大量接口的情况,使代码太过碎版化;粒度太大,解耦效果不好。同时对于UI的输入和数据的变化,需要手动调用V层或者P层相关的接口,相对来说缺乏自动性、监听性。如果数据的变化能自动响应到UI、UI的输入能自动更新到数据,那该多好!
- MVP是以UI为驱动的模型,更新UI都需要保证能获取到控件的引用,同时更新UI的时候要考虑当前是否是UI线程,也要考虑Activity的生命周期(是否已经销毁等)。
- MVP是以UI和事件为驱动的传统模型,数据都是被动地通过UI控件做展示,但是由于数据的时变性,我们更希望数据能转被动为主动,希望数据能更有活性,由数据来驱动UI。
- V层与P层还是有一定的耦合度。一旦V层某个UI元素更改,那么对应的接口就必须得改,数据如何映射到UI上、事件监听接口这些都需要转变,牵一发而动全身。如果这一层也能解耦就更好了。
- 复杂的业务同时也可能会导致P层太大,代码臃肿的问题依然不能解决。
MVP的变种:Passive View
MVP的变种有很多,其中使用最广泛的是Passive View模式,即被动视图。在这种模式下,View和Model之间不能直接交互,View通过Presenter与Model打交道。Presenter接受View的UI请求,完成简单的UI处理逻辑,并调用Model进行业务处理,并调用View将相应的结果反映出来。View直接依赖Presenter,但是Presenter间接依赖View,它直接依赖的是View实现的接口,相对于View的被动,那Presenter就是主动的一方。对于Presenter的主动,有如下的理解:
- Presenter是整个MVP体系的控制中心,而不是单纯的处理View请求的人;
- View仅仅是用户交互请求的汇报者,对于响应用户交互相关的逻辑和流程,View不参与决策,真正的决策者是Presenter;
- View向Presenter发送用户交互请求应该采用这样的口吻:“我现在将用户交互请求发送给你,你看着办,需要我的时候我会协助你”,不应该是这样:“我现在处理用户交互请求了,我知道该怎么办,但是我需要你的支持,因为实现业务逻辑的Model只信任你”;
- 对于绑定到View上的数据,不应该是View从Presenter上“拉”回来的,应该是Presenter主动“推”给View的;
- View尽可能不维护数据状态,因为其本身仅仅实现单纯的、独立的UI操作;Presenter才是整个体系的协调者,它根据处理用于交互的逻辑给View和Model安排工作。
MVP实践
MVC → MVP
当我们将Activity复杂的逻辑处理移至另外的一个类(Presenter)中时,Activity其实就是MVP模式中的View,它负责UI元素的初始化,建立UI元素与Presenter的关联(Listener之类),同时自己也会处理一些简单的逻辑(复杂的逻辑交由 Presenter处理)。
MVP的Presenter是框架的控制者,承担了大量的逻辑操作,而MVC的Controller更多时候承担一种转发的作用。因此在App中引入MVP的原因,是为了将此前在Activty中包含的大量逻辑操作放到控制层中,避免Activity的臃肿。
两种模式的区别:
- (最主要区别)View与Model并不直接交互,而是通过与Presenter交互来与Model间接交互,从而实现了这两者之间完全的解耦。而在MVC中View可以与Model直接交互
- 通常View与Presenter是一对一的,但复杂的View可能绑定多个Presenter来处理逻辑。而Controller是基于行为的,并且可以被多个View共享,Controller可以负责决定显示哪个View
- Presenter与View的交互是通过接口来进行的,更有利于添加单元测试
MVVM
MVVM最早应用于Windows的WPF中,在安卓上使用该框架需要对谷歌的data-binding框架有一定的了解才能熟练使用。MVVM可以算是MVP的升级版,其中的VM是ViewModel的缩写,ViewModel可以理解成是View的数据模型和Presenter的合体,ViewModel和View之间的交互通过Data Binding完成,而Data Binding可以实现双向的交互,这就使得视图和控制层之间的耦合程度进一步降低,关注点分离更为彻底,同时减轻了Activity的压力。但是由于去除了Presenter层,会导致view层依然过重,所以衍生了其他类似于MVPVM框架。
刚开始理解这些概念的时候认为这几种模式虽然都是要将view和model解耦,但是非此即彼,没有关系,一个应用应该只会用一种模式。后来发现这几种模式的边界并非那么明显,可能你在自己的应用中都会用到。实际上也根本没必要去纠结自己到底用的是MVC、MVP还是MVVP,不管黑猫白猫,捉住老鼠就是好猫。
MVC → MVP → MVVM 的演进过程
MVC → MVP → MVVM 这几个软件设计模式是一步步演化发展的,MVVM 是从 MVP 的进一步发展与规范,MVP 隔离了MVC中的 M 与 V 的直接联系后,靠 Presenter 来中转,所以使用 MVP 时 P 是直接调用 View 的接口来实现对视图的操作的,这个 View 接口的东西一般来说是 showData、showLoading等等。
M 与 V 已经隔离了,方便测试了,但代码还不够优雅简洁,所以 MVVM 就弥补了这些缺陷。在 MVVM 中就出现的 Data Binding 这个概念,意思就是 View 接口的 showData 这些实现方法可以不写了,通过 Binding 来实现。
- 同 → 如果把这三者放在一起比较,先说一下三者的共同点,也就是Model和View:
Model:数据对象,同时,提供本应用外部对应用程序数据的操作的接口,也可能在数据变化时发出变更通知。Model不依赖于View的实现,只要外部程序调用Model的接口就能够实现对数据的增删改查。
View:UI层,提供对最终用户的交互操作功能,包括UI展现代码及一些相关的界面逻辑代码。 - 异 → 三者的差异在于如何粘合View和Model,实现用户的交互操作以及变更通知
Controller
Controller接收View的操作事件,根据事件不同,或者调用Model的接口进行数据操作,或者进行View的跳转,从而也意味着一个Controller可以对应多个View。Controller对View的实现不太关心,只会被动地接收,Model的数据变更不通过Controller直接通知View,通常View采用观察者模式监听Model的变化。
Presenter
Presenter与Controller一样,接收View的命令,对Model进行操作;与Controller不同的是Presenter会反作用于View,Model的变更通知首先被Presenter获得,然后Presenter再去更新View。一个Presenter只对应于一个View。根据Presenter和View对逻辑代码分担的程度不同,这种模式又有两种情况:Passive View和Supervisor Controller。
ViewModel
注意这里的“Model”指的是View的Model,跟MVVM中的一个Model不是一回事。所谓View的Model就是包含View的一些数据属性和操作的类,这种模式的关键技术就是数据绑定(data binding),View的变化会直接影响ViewModel,ViewModel的变化或者内容也会直接体现在View上。这种模式实际上是框架替应用开发者做了一些工作,开发者只需要较少的代码就能实现比较复杂的交互。
一点总结
- MVP和MVVM完全隔离了Model和View,但是在有些情况下,数据从Model到ViewModel或者Presenter的拷贝开销很大,可能也会结合MVC的方式,Model直接通知View进行变更,甚至还有衍生出MVPVM的架构模式。在实际的应用中很有可能你已经在不知不觉中将几种模式融合在一起,但是为了代码的可扩展、可测试性,必须做到模块的解耦,不相关的代码不要放在一起。其实只要一个框架提供了视图和模型分离的功能,我们的目的就达到了。在开发深入之后,可以再体会用到的框架到底是MVC、MVP还是MVVM。
- 因为做了视图与逻辑和数据模型的抽离,所以必然在原有代码的基础上增加很多类,但是整个代码的结构却反而会更加清晰,项目也更易于维护和扩展
- 最佳实践:MVP+data-binding,既使用了data binding框架去节省了类似findViewById和数据绑定的时间,又使用了presenter去将业务逻辑和view层分离
参考文章
Android App的设计架构:MVC,MVP,MVVM与架构经验谈
浅谈Android架构之MVP,MVVM
认清Android框架 MVC,MVP和MVVM
如何构建Android MVVM 应用框架