为什么要学习架构?
不管是MVC还是MVP,亦或则其他架构,它们的设计目的都是为了达到编码的最高境界,那就是:低藕合,高复用,易测试,好维护。
而要达到这个终极目标,首先要理解的是每个部分各自负责些什么,以及如何组合在一起。因此我个人认为,学习架构关键在两步:
- 如何把缠在一起的代码拆分。
- 如何把拆开的代码再组合。
很多新手在刚做项目时,都会把所有的代码,如数据的访问和处理,数据的展示,用户的输入都写在一起,代码和思维都呈现出一种线性的形式,一切都是直来直往的。这样代码量确实少,写的时候也觉得方便,但是带来了几个致命的问题:
- 当业务越来越复杂时,修改代码成了噩梦。同样的逻辑放在不同的地方,本来只用改一处的变成了需要改几百处。而又因为所有的逻辑都互相牵扯住,导致本来只想改一处的,结果却影响了几百处。
- 当时间越来越遥远时,理解代码成了噩梦。本来只需要阅读几行的时候,却因为所有的代码都杂在一起变成了要阅读几千行。时间一长,重新阅读时,别说别人,就是自己也很难一下就能掌握关键点。
- 当需要做一个功能时,却发现代码无法复用,明明是一样的逻辑也只能靠
Ctrl+C
加Ctrl+V
。这又为今后修改代码时增加了工作量。 - 当需要测试时确发现很难进行测试,每次修改一处代码后都必须进行重复的人工测试,无法进行自动化测试,模块和模块也无法拆开来进行独立的单元测试,每次都要整体的测一遍才能确保一切完好如初。
要换的不是架构,而是思维方式
其实目前市面上的架构模式已经有很多种,各有不同,但模式终究只是一种设计理念的表现形式,学习再多的架构,你也只是多会用了几种工具而已,学习一种模式其实是在学一种思维方式:
如何在解决问题的时候把问题合理的拆分,又如何将拆分的零件合理的组装成解决问题的工具。
将这种思维方式深入到你的大脑里,血液里,直至骨髓,让它成为你思考问题的一种习惯和定式,逐渐成为你的一部分,那么这时你已达到无招胜有招的境界了。
闲话先不扯了,回正题。
实现架构没有唯一标准
这里首先需要说明的是无论MVP或MVC还是MVVM等任何一种架构和模式其实都没有谁优谁劣之分,而且就算是同一种架构,也可以根据不同的使用场景来做不同的实现方式,这里并没有宇宙绝对的对错标准和概念定义。这和张三丰在教无忌太极拳以后让其先忘掉招式是一样的道理,在应用型领域,定式和概念只应是在学习的过程中才存在的,而真正学会以后应该马上忘掉定式,忘掉概念,将其用熟用活才是关键。
所以说我在此描述的概念也不是唯一的标准,而只是个人对其的理解。
什么是MVC
因为MVP是MVC的一个变种,因此我们先来看在MVC里代码是如何拆分和组合的,这里简要的描述常见的定义:
- 拆分:Model负责数据,View负责显示,Controller负责用户输入。
- 组合:View将用户操作和事件传递给Controller,Controller将用户命令翻译成消息来传递给Model,Model在处理完数据后,通知View来显示给用户,而View又从Model取出数据。
我认为在学习MVP之前,必须深刻理解什么叫做MVC,因为两者的区别其实没有你想象中的那么大,与其神话并盲目追崇新的架构,期望其能解脱你于苦海,还不如深刻的理解MVC的设计理念,这就和学习一门新语言一样,只有你真正深刻的理解了其思维方式,那么学习新的一门语言其实都是易如反掌的。因为它们其实都是在做一件事,只是做的过程上有些许差异而已。
更多MVC的理解,可以参考我之前写的一篇文章:在谈MVP之前,你真的懂MVC吗?
什么是MVP
MVP与MVC最大的区别就在与将Model和View通过Presenter隔开了,不再允许其互相直接通信,而所有的消息都是通过Presenter这个中间人来传递。
而这样做的目的主要是为了将数据和展示划出更明确的界限。
首先我们来看MVP到底是在解决什么问题的:
- 数据管理
- 什么是数据?(Model)
- 如何筛选数据?(Selections)
- 如何改变数据?(Commands)
- 用户界面
- 如何展示数据?(View)
- 如何通过事件来改变数据?(Interactor)
- 如何将这些放在一起?(Presenter)
可以看出其实一个GUI程序的核心,还是在于用户和数据之间的关系。而架构的引入也是为了解决这几个核心的问题。
那么MVP又是如何解决这几个问题的,Model,View,Presenter三者又各自负责什么呢?谁来负责请求网络数据,访问和存储本地数据,谁来负责处理数据,谁来负责显示数据,谁又来负责和用户交互呢?
具体表现在代码上,也可以说:网络请求应该放在哪,数据库访问应该放在哪,业务逻辑应该放在哪里,处理用户输入应该放在哪,谁又来控制显示或隐藏View的等具体的细节问题。
带着这些具体问题,我们一起来学习。
如何拆分
首先来看MVP各自负责什么:
- Model,负责定义数据(解决什么是数据)
- Presenter, 负责在Model和View之间,从model里取出数据,格式化后在View上展示(解决如何把数据和用户界面放在一起)。
- View,负责担任一个被动界面,用于展示数据。(解决如何展示数据)
和MVC比较而已,这里出现一个最大的疑问就是:那么谁来负责和用户的操作交互呢?答案是,用户在View上的所有操作(事件)都由View路由给Presenter,由Presenter来与其交互。
而和MVC最大的区别在于Model和View完全被Presenter隔开了,Presenter作为它们之间的中间人去传递所有数据。
如何组合
三者又是如何组合起来的呢?
很显然Presenter作为中间者,它是同时拥有View和Model的引用的,为了在它们之间起到桥梁作用,即Presenter会主动和View和Model进行通信。
而Model和View必须是完全隔离的,不允许两者之间互相通信,保持对彼此的不感知,这样的好处是你彻底将数据和展示分离来开,并且可以独立的为Model去做测试。
Model在三者中是独立性最高的,Model不应该拥有对View的引用,而且Model也不需要保存对Presenter的引用,对于Presenter而已,Model只需要提供接口,等着Presenter来调用时返回相应数据即可,这和经典MVC模式中是非常不同的,在MVC中Model在数据发送变化后,是需要发送广播来告之View去更新用户界面的,而在MVP中,Model是不应该去通知View,而是通知Presenter来间接的更新View的。
而Presenter和Model的关系也应该是基于接口来通信,这样才能把Model和Presenter的耦合度也降到最低,那么在需要改变Model内部实现,甚至彻底替换Model的时候,Presenter则是无需随之改变的。这样做带来的另一个好处就是你可以通过Mock一个Model来对Presenter以及View做模拟测试了,从而提高了可测试性。
那么View和Presenter的关系呢?View是需要拥有对Presenter的引用,但仅仅是为了将用户的操作和事件立即传递给Presenter,为了让View和Presenter耦合较低,View也只应该通过接口与Presenter通信,从而保证View是完全被动的,一方面它由用户的操作触发来和Presenter通信,另一方面它完全受Presenter控制,唯一需要做的事情就是如何展示数据。
简要总结三者之间的关系是:View和Model之间没有联系,View通过接口向Presenter来传递用户操作,Model不主动和Presenter联系,被动的等着Presenter来调用其接口,Presenter通过接口和View/Model来联系。
View <- 接口 <- Presenter ->接口 -> Model
View -> 接口 -> Presenter <- 接口 <- Model
Talk is cheap, show me the code
为了便于理解,这里提供一些伪代码加注释演示一下如何实现MVP模式:
View
interface IUserView {
void setPresenter(presenter);
void showUsers(users);
void showDeleteUserComplete();
void showDeleteUserError();
}
class UserView implements IUserView {
UserPresenter presenter;
// 保持对Presenter的引用,用于路由用户操作
void setPresenter(presenter) {
this.presenter = presenter;
}
// 将Presenter传递来的数据展示出来
void showUsers(users) {
draw(users);
}
// Model操作数据成功后,通过Presenter来告之View需要更新用户界面
void showDeleteUserComplete() {
alert("Delete User Complete");
}
// Model操作数据失败后,也是通过Presenter来告之View需要更新用户界面
void showDeleteUserError() {
alert("Delete User Fail");
}
// 当用户点击某个按钮时,将用户操作路由给presenter,由presenter去处理
void onDeleteButtonClicked(event) {
presenter.deleteUser(event);
}
}
Model
interface IUserModel {
List<User> getUsers();
boolean deleteUserById();
}
class UserModel implements IUserModel {
// 在数据库里查找数据,并将数据返回给presenter
List<User> getUsers() {
return getUsersInDatabase(id);
}
// 在数据库里删除数据,并将结果返回给presenter
User deleteUserById(id) {
return deleteUserByIdInDatabase(id);
}
}
Presenter
interface IUserUserPresenter {
void deleteUser(event);
}
class UserUserPresenter implements IUserPresenter {
// 保持对View的引用
IUserView view;
// 保持对Model的引用
IUserModel model;
UserUserPresenter(IUserView view, IUserModel model) {
this.view = view;
this.model = model;
this.view.setPresenter(this);
}
void start() {
// 从Model中取出数据
List<User> users = model.getUsers();
// 将数据发送给View,让其展示到用户界面
view.showUsers(users);
}
void deleteUser(event) {
// View将用户操作路由过来,由Presenter来处理
long uid = whichUserNeedToDeleteBy(event);
// 将用户操作翻译成命令或消息传递给model,以改变数据
boolean success = model.deleteUserById(uid);
// 将Model操作数据后的结果通知View来改变用户界面
if (success) {
view.onDeleteUserSuccess();
} else {
view.onDeleteUserFail();
}
}
}
OK,到此一个最简单的MVP模式就实现了,可以看到整个架构的轮廓已经很清晰了,而且面向接口编程也带来了很多的好处,让代码之间藕和较少,对测试支持也更为友好,理解和维护起来也更加方便了。
以后有时间再为大家描述如何在android里面实现MVP模式,以便更具体的理解。有疑问欢迎参与讨论,大家一起学习进步。