我所理解的MVP模式
前几天看了一些关于MVP的一些文章,发展每个人都有自己的见解,并不能达到高度的统一,不过笔者还是大受裨益。今天笔者也谈谈自己对MVP的一些理解,供大家学习交流。
由于笔者是一枚iOS开发,所以接下来有些地方会结合iOS来,以期将笔者想说的都讲的清楚明白。
MVP与其说是设计模式,还不如说是一种程序架构范式,说明了我们应该怎样更好的组织整个项目的代码和资源。
MVP模块关系图M:Model层,数据服务层,负责数据的增删改查。在服务端就包括mysql数据库操作、本地cache等;在客户端就包括调用服务器API、各种形式的数据缓存等。有些朋友将Model层理解为数据模型层,数据模型归根结底是数据,设计数据模型是为了在面向对象的程序设计中更好的表达数据。所以数据模型贯穿于整个应用程序,不应该将Model层单单理解为是数据模型。但Model层往往负责将接收到的数据转化为相应的数据模型供上层使用。
V:View层,视图界面层,负责UI的渲染、子视图的组织、UI事件、用户交互等。在有些网友看来,View层是比较轻的一层,大多数时候只需要调用系统的UI控件,绑定需要的UI事件就完事了,很多平台甚至拖拖控件就行。但是实际上是因为平台为我们做了大多数的事,我们不需要去考虑怎么有效的渲染界面,也不需要去考虑怎样去实现各种各样的交互事件(触摸),只需要关注应用本身就可以了。
P:Presenter层,有得朋友将其叫作发布者,百度翻译为主持人,笔者觉得后者更贴切些。 Presenter既是中间人,在View和Model之间起到桥梁的作用,又是一个独立的大模块,封装了业务的复杂度,将UI和业务逻辑拆分开来,使UI和业务都可以独立的进行变化。从整体的数据流向上看,Presenter从Model层获取数据,并通过接口发送给View层展示;View层将用户交互传递给Presenter,由Presenter完成相应的业务逻辑,这其中可能会有Model层的参与,比如Presenter调用Model层的接口来保存数据。
面向接口编程的MVP在理想状态下,Model、View、Presenter这三层都应该是面向接口(interface/protocol)编程的,从而达到低耦合,高复用的效果。同时,由于业务不依赖于UI,使单元测试也更容易进行。很多人并没有单元测试的重要性,一个网友描述的挺好的,这里直接将原话贴出来:
上面都说了,完全面向接口编程是最理想的状态。实际情况则是过份的设计大大增加了开发过程中的复杂度,降低了开发的效率,最终可能影响项目的进度。所以在实际的项目开发中,对MVP的应用深度应该视情况而定:
- 当某一个View需要在项目中的多处复用而View本身相对来说比较复杂时,P层应该面向接口编程,而View只是持有特定接口类型的引用。这里推荐采用代理模式,即在View层定义一个特定的Protocol,让P层所有需要与该View层对接的类都去实现这个Protocol以便接收View层发送过来的事件,这样同一个View层可以对接多个不同的P层,View可以方便的进行复用。如:UITableView等。
- 当我们需要针对同一个业务经常更换用户界面时,为了让每一次改动都不会对P层造成大的影响,在设计View层时则需要完全面向接口进行设计,P层持有IView接口的引用,这样不管View层的具体类怎么变动,View层和P层的交互始终依赖的是一直未变动的程序接口IView,P层则不需要做大的改动。
- 如果你没有诸如切换数据存储方式(mysql->oracle, file->sqlite)等需求时,往往不需要用面向接口的方式来设计Model层。但有一点还是应该留意:iOS开发中,大多数情况下,我们在Model层都有两件事去做,一是调用网络接口访问服务器,再是本地缓存。数据既可以来自于网络也可以来自于缓存,但这些细节对P层来说应该是透明的,我们不能将获取数据的这两种方式都暴露给P层,而是需要以此为基础设计统一的接口。这样做的好处是,一方面简化了P层对Model层的调用,API操作数据具有一致性;另一方面,更改缓存机制,调整网络接口都不会对P层产生影响。
笔者认为,实现MVP模式的最低原则是:
- View和Model之间不能直接进行交互,必须通过Presenter来交流数据;
- 尽量的将业务逻辑和UI展示分开;
- 尽量使用面向接口的方式来实现MVP三层,特别是对于View与Presenter之间的交互。
在iOS平台上,目前大多数采用的MVC的实现,实际上已经符合了MVP的特点了,只是ViewController做了更多的事。ViewController充当了Presenter的角色,在View和Model之间起到了桥梁的作用,同时封装了业务逻辑。除此之外,ViewController还负责界面的生命周期、视图组织、页面跳转等。ViewController虽然属于View层,但很多时候我们还是会用它来封装业务逻辑,由于承担了过多的职责,在有些复杂的情况下,ViewController往往变得不堪重负,给维护代码和新增功能都带来了不利影响。为了给ViewController瘦身,我们可以采取以下措施,供参考:
- 将View的展示逻辑放到相应的View中。典型的就是UITableView的delegate和datasource,我们可以自定义UITableView的子类,将相应的代理方式都移植到子类中去,然后暴露一个接收数据的方法供ViewController调用,这样来给ViewController瘦身。
- 将复杂的View-View交互剥离并封装起来。比如界面上的UIScrollView滚动时,我们需要不断的调整界面头部的透明度。这些View之间的交互本身并不涉及具体的业务逻辑,我们就可以将其封装起来,达到使ViewController瘦身的目的。另外,如果View-View的交互具有一般性的话,我们还可以在其他需要的地方方便的复用这种交互行为,一举两得。
- 统一处理界面跳转。界面跳转放到ViewController中再合适不过了,ViewController天生就具备这样的能力。我们要做的,就是尽量将跳转统一封装到基类中,而不要同样的跳转逻辑在每个具体的VC中都去写一遍。
- 如果一个ViewController包含多个复杂的业务逻辑,我们应该为这些定义多个Presenter来分别进行处理。ViewController本身只需要创建View和Presenter,并将具体的View将具体的Presenter对应起来就行了。这样,ViewController就变得孑然一身了。
最后给出一个iOS平台上实现MVP模式的例子,供大家参考:
由于代码比较多,这里只贴出主要的接口代码,项目源码请查看https://github.com/yuexygoodman/MVPExample
1、登录界面,View层接口
@protocol ILoginView<NSObject>
- (void)setPresenter:(id<ILoginPresenter>)presenter;
- (void)loadWithLastAccount:(NSString *)account pwd:(NSString *)pwd;//显示历史账号
- (void)showLoginError:(NSString *)err;//显示登陆错误
- (void)showLoading;//显示正在登陆
- (void)hideLoading;//登陆完成后隐藏
- (void)navToMain;//切换界面到主界面
@end
2、登录界面 Presenter接口
@protocol ILoginPresenter<NSObject>
- (void)setLoginView:(id<ILoginView>)loginView;
- (void)start;
- (void)onLoginWithAccount:(NSString *)account pwd:(NSString *)pwd;//处理登录逻辑
@end
3、朋友管理界面 View层接口
@protocol IFriendView<NSObject>
- (void)setPresenter:(id<IFriendPresenter>)presenter;
- (void)loadWithFriends:(NSArray<Friend *> *)friends;//显示列表
- (void)confirmFriend:(Friend *)fri msg:(NSString *)msg;//向用户展示再次确认窗口
- (void)unShowingForFriend:(Friend *)fri;//取消被删除朋友的显示
- (void)showRemoveError:(NSString *)msg;//显示错误
4、朋友管理界面 Presenter接口
@protocol IFriendPresenter<NSObject>
- (void)setFriendView:(id<IFriendView>)friendView;
- (void)start;//启动
- (void)onRemoveFriend:(Friend *)fri;//删除业务
- (void)onSureRemoveFriend:(Friend *)fri;//直接删除
@end
再次给出Demo地址:https://github.com/yuexygoodman/MVPExample 欢迎大家访问
对于MVP模式的理解就到此为止了,下一篇笔者准备继续说一说MVVM,以上内容,均属笔者拙见。有什么不对的地方,烦请指教。