MVP模式是你的救命稻草吗?

为什么要学习架构?

不管是MVC还是MVP,亦或则其他架构,它们的设计目的都是为了达到编码的最高境界,那就是:低藕合,高复用,易测试,好维护。

而要达到这个终极目标,首先要理解的是每个部分各自负责些什么,以及如何组合在一起。因此我个人认为,学习架构关键在两步:

  1. 如何把缠在一起的代码拆分
  2. 如何把拆开的代码再组合

很多新手在刚做项目时,都会把所有的代码,如数据的访问和处理,数据的展示,用户的输入都写在一起,代码和思维都呈现出一种线性的形式,一切都是直来直往的。这样代码量确实少,写的时候也觉得方便,但是带来了几个致命的问题:

  1. 当业务越来越复杂时,修改代码成了噩梦。同样的逻辑放在不同的地方,本来只用改一处的变成了需要改几百处。而又因为所有的逻辑都互相牵扯住,导致本来只想改一处的,结果却影响了几百处。
  2. 当时间越来越遥远时,理解代码成了噩梦。本来只需要阅读几行的时候,却因为所有的代码都杂在一起变成了要阅读几千行。时间一长,重新阅读时,别说别人,就是自己也很难一下就能掌握关键点。
  3. 当需要做一个功能时,却发现代码无法复用,明明是一样的逻辑也只能靠Ctrl+CCtrl+V。这又为今后修改代码时增加了工作量。
  4. 当需要测试时确发现很难进行测试,每次修改一处代码后都必须进行重复的人工测试,无法进行自动化测试,模块和模块也无法拆开来进行独立的单元测试,每次都要整体的测一遍才能确保一切完好如初。

要换的不是架构,而是思维方式

其实目前市面上的架构模式已经有很多种,各有不同,但模式终究只是一种设计理念的表现形式,学习再多的架构,你也只是多会用了几种工具而已,学习一种模式其实是在学一种思维方式:

如何在解决问题的时候把问题合理的拆分,又如何将拆分的零件合理的组装成解决问题的工具。

将这种思维方式深入到你的大脑里,血液里,直至骨髓,让它成为你思考问题的一种习惯和定式,逐渐成为你的一部分,那么这时你已达到无招胜有招的境界了。

闲话先不扯了,回正题。

实现架构没有唯一标准

这里首先需要说明的是无论MVP或MVC还是MVVM等任何一种架构和模式其实都没有谁优谁劣之分,而且就算是同一种架构,也可以根据不同的使用场景来做不同的实现方式,这里并没有宇宙绝对的对错标准和概念定义。这和张三丰在教无忌太极拳以后让其先忘掉招式是一样的道理,在应用型领域,定式和概念只应是在学习的过程中才存在的,而真正学会以后应该马上忘掉定式,忘掉概念,将其用熟用活才是关键。

所以说我在此描述的概念也不是唯一的标准,而只是个人对其的理解。

什么是MVC

因为MVP是MVC的一个变种,因此我们先来看在MVC里代码是如何拆分和组合的,这里简要的描述常见的定义:

  1. 拆分:Model负责数据,View负责显示,Controller负责用户输入。
  2. 组合:View将用户操作和事件传递给Controller,Controller将用户命令翻译成消息来传递给Model,Model在处理完数据后,通知View来显示给用户,而View又从Model取出数据。

我认为在学习MVP之前,必须深刻理解什么叫做MVC,因为两者的区别其实没有你想象中的那么大,与其神话并盲目追崇新的架构,期望其能解脱你于苦海,还不如深刻的理解MVC的设计理念,这就和学习一门新语言一样,只有你真正深刻的理解了其思维方式,那么学习新的一门语言其实都是易如反掌的。因为它们其实都是在做一件事,只是做的过程上有些许差异而已。

更多MVC的理解,可以参考我之前写的一篇文章:在谈MVP之前,你真的懂MVC吗?

什么是MVP

MVP与MVC最大的区别就在与将Model和View通过Presenter隔开了,不再允许其互相直接通信,而所有的消息都是通过Presenter这个中间人来传递。

而这样做的目的主要是为了将数据和展示划出更明确的界限。

首先我们来看MVP到底是在解决什么问题的:

  • 数据管理
    1. 什么是数据?(Model)
    2. 如何筛选数据?(Selections)
    3. 如何改变数据?(Commands)
  • 用户界面
    1. 如何展示数据?(View)
    2. 如何通过事件来改变数据?(Interactor)
    3. 如何将这些放在一起?(Presenter)

可以看出其实一个GUI程序的核心,还是在于用户和数据之间的关系。而架构的引入也是为了解决这几个核心的问题。

那么MVP又是如何解决这几个问题的,Model,View,Presenter三者又各自负责什么呢?谁来负责请求网络数据,访问和存储本地数据,谁来负责处理数据,谁来负责显示数据,谁又来负责和用户交互呢?

具体表现在代码上,也可以说:网络请求应该放在哪,数据库访问应该放在哪,业务逻辑应该放在哪里,处理用户输入应该放在哪,谁又来控制显示或隐藏View的等具体的细节问题。

带着这些具体问题,我们一起来学习。

如何拆分

首先来看MVP各自负责什么:

  1. Model,负责定义数据(解决什么是数据)
  2. Presenter, 负责在Model和View之间,从model里取出数据,格式化后在View上展示(解决如何把数据和用户界面放在一起)。
  3. 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模式,以便更具体的理解。有疑问欢迎参与讨论,大家一起学习进步。

参考文档

  1. Model–view–presenter WIKI
  2. MVP: Model-View-PresenterThe Taligent Programming Model for C++ and Java
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • 前言 看了下上篇博客的发表时间到这篇博客,竟然过了11个月,罪过,罪过。这一年时间也是够折腾的,年初离职跳槽到鹅厂...
    西木柚子阅读 21,110评论 12 185
  • 作者:李旺成 时间:2016年4月3日 “Android MVP 详解(下)”已经发布,欢迎大家提建议。 MVP ...
    diygreen阅读 128,662评论 86 1,321
  • 没有督促的压力好像变成了混日子,少了主动,也丧失了方向,更重要的是感觉到了无用。如果做的事情没了价值,或者没了可以...
    聂一一阅读 102评论 0 0
  • 正常情况下,在使用CoreGraphics框架中的方法进行图形绘制时,每一闭合的图形都是一个独立的层,如果在绘制时...
    F麦子阅读 319评论 0 0