MVP 架构模式是一种常用于iOS 应用的软件架构模式。它可以帮助开发者在应用程序中实现业务逻辑和用户界面的分离,以便更轻松地管理和修改应用程序的各个部分。在这篇文章中,我将详细介绍MVP架构模式的起源、核心思想、与MVC模式的区别以及各个模块的职能和分工。
本文也将以知乎日报的App 来作为学习MVP 模式的Demo。
1 起源
之前学习过的经典MVC 模式下,有很多的优点:
- 代码清晰,职责分明,易于维护。
- 代码复用性高,模型和视图可以在多个控制器中重复使用。
但是随着业务的提升,代码量,复杂程度也随之提升,MVC 模式的缺点也随之暴露出来:
- 控制器往往会变得过于臃肿,逻辑代码和业务代码难以分离。
- 视图和模型之间的交互逻辑很难在多个控制器中进行共享和复用。
- 难以进行单元测试,因为控制器往往依赖于视图和模型。
总结来看,在iOS 应用的开发中,开发者通常需要处理很多业务逻辑和界面的代码,这些代码经常混杂在一起,很难维护。随着iOS 应用程序的复杂性和规模不断增加,需要更好的架构来管理这些代码。
MVP 架构模式的诞生正是为了解决这个问题。MVP 架构模式将应用程序分成三个主要部分:模型(Model)、视图(View)和表示器或者叫模型视图协调器(Presenter)。这种架构模式使开发者可以更好地管理代码,提高应用程序的可维护性和可扩展性。
2 简单介绍
Model
通过上图我们也可以看出,MVP 模式下的Model 和MVC 模式下的Model 没有非常大的区别,都是负责处理应用程序的数据,包括数据的获取、存储和处理等。在MVP架构中,模型通常是指业务逻辑、数据层或网络层。 但是在数据的逻辑处理方面,相较于在MVC 模式中学习过的胖Model 和瘦Model,有了一些不同的做法。这个会在下文中提及。
View
上面的关系图中,我们可以看到与MVC 模式非常大的不同,那就是ViewController 变成了View 层的一部分。 View 负责显示应用程序的用户界面,并向用户提供交互功能。在MVP 架构中,视图通常是指用户界面、UI 控件或视图控制器。
Presenter
这是MVP 架构模式中最重要的一点,Presenter 是Model 和View 之间的桥梁,它使得Model 和View完全独立,在MVC 模式下ViewController 的业务代码,在MVP 模式下由Presenter 完成,使得View 层中的ViewController 能够更加专注的完成展示UI,处理用户交互。
什么是业务代码:
1.数据处理:从Model层获取数据,对数据进行处理、转换和组织,以符合View层的需求。
这就是上文提到的在MVC 模式下原来是Model 进行的数据的处理,在MVP 模式中交给了Presenter,原因就是Presenter 要把Model 上请求到的原始数据转化成View 层上直接可以使用的数据。
举个栗子:
如果从网络上请求下来的原始数据是“20230421”,但是UI 上需要的是“4月21日”这种数据的处理,在MVC 模式下,可以写在Model 里面。这样可以使得ViewController 更加专注于视图的展示和交互,而不需要关心数据处理的具体实现。
但在MVP 模式下,Model层通常负责数据的获取、存储和处理,对于一些数据处理和存储的操作,应该放在Model 层中实现,比如数据的解析、数据的加密和解密等等。
但是对于一些业务逻辑相关的数据处理,比如对数据进行计算、筛选、排序等,这些数据处理操作可能需要考虑到多种情况,需要根据当前业务逻辑来判断处理结果,这时候就可以将这些操作放在Presenter 层中实现。因为Presenter 层是负责处理视图展示和业务逻辑的,所以它更容易理解当前的业务流程和需求,更能灵活地处理数据。
因此,把“20230412”转化成“4月12日”这种数据的处理,在MVP 模式下,应该放在Presenter 中实现,处理完后,可以直接交给View 层展现给用户。
2.业务流程:负责控制应用程序的业务流程,根据用户交互和其他条件,触发不同的业务逻辑,协调Model和View之间的交互,保证业务流程的正确性和一致性。
3.错误处理:处理业务逻辑中出现的错误和异常,例如网络连接失败、数据格式错误、用户输入错误等。Presenter 应该能够对这些错误进行正确处理,给用户提供友好的提示和解决方案。
举例来说,在知乎日报中,Presenter层的业务逻辑包括:
1.从Model层获取最新新闻列表数据,并根据View层的需要进行处理和组织,例如将数据按照日期分组、提取每个新闻的标题和图片等。
2.控制新闻详情页面的显示和刷新,根据用户的点击或者手势操作,触发不同的业务逻辑,例如加载更多评论、收藏新闻等。
3.处理网络请求失败、数据解析错误等异常情况,例如当网络连接失败时,Presenter 应该能够正确处理这种异常情况,给用户友好的提示,或者自动重试连接等。
总结一些MVP 的规范
这里提供一种目前比较多人使用的规范:
View
层是由UIViewController
和UIView
共同组成;View
层将委托Presenter
层对它自己的操作;Presenter
层拥有对View
层交互的逻辑;Presenter
层跟Model
层通信,并将数据转化成对适应UI的数据并更新View
;Presenter
不需要依赖UIKit
;View
层是单一,因为它是被动接受命令,没有主动能力。
3 项目代码例子
在基本了解了MVP 架构中各部门的分工后,我们看看怎么在代码中体现MVP 模式。
有人有说,MVP 架构的中心思想是面向接口编程,调用层使用接口对象,去调用接口方法,实现层去实现接口方法,并在调用层实例化。的确,MVP 模式最精彩的部 分就是接口的使用。
Model
在Model 中,不仅要请求网络数据,同时要提供接口给Presenter。
@protocol NewsModelDelegate <NSObject>
@optional
/// 请求Latest 成功的回调
/// - Parameter latestDayModel: 最新新闻的model
- (void)didReceiveLatestNews:(DayModel *)latestDayModel;
/// 请求before 成功的回调
/// - Parameter beforeDayModel: 最新新闻的model
- (void)didReceiveBeforeNews:(DayModel *)beforeDayModel;
@end
@interface NewsModel : NSObject
/// 代理
@property (nonatomic, weak) id<NewsModelDelegate> delegate;
/// 获取最新新闻
- (void)fetchLatestNews;
/// 获取过往新闻
- (void)fetchBeforeNewsWithDate:(NSString *)date;
@end
@implementation NewsModel
/// 获取最新新闻
- (void)fetchLatestNews {
[DayModel getLatest:^(DayModel * _Nonnull latestModel) {
[self.delegate didReceiveLatestNews:latestModel];
}];
}
/// 获取过往新闻
- (void)fetchBeforeNewsWithDate:(NSString *)date {
[DayModel getBeforeDate:date AndModel:^(DayModel * _Nonnull beforeModel) {
[self.delegate didReceiveBeforeNews:beforeModel];
}];
}
@end
在上面的代码中,Model 为Presenter 提供了接口的实现,在Presenter中,会持有Model,(同时会遵守NewsModelDelegate
协议) 通过调用Model的fetchLatestNews
和fetchBeforeNewsWithDate:(NSString *)date
方法,会实现didReceiveLatestNews:(DayModel *)latestDayModel
和didReceiveBeforeNews:(DayModel *)beforeDayModel
方法,从而获取到网络请求到的数据,再展现给View 层展示。
Protocol
@protocol MainProtocol <NSObject>
/// 展示最新新闻
- (void)showLatestNews:(NSDictionary *)latestModel;
/// 展示过往新闻
- (void)showBeforeNews:(NSArray *)beforeModel;
@end
这里的协议也相当于接口,View 层遵守这个协议,而Presenter 需要一个遵守这个协议的对象,这样能更加表达Presenter 在传递Model 层的数据时,不需要知道View 层是谁,只需要知道这是一个遵守了MainProtocol
的对象,这样也符合了我们上面提到的MVP 规范:
View
层将委托Presenter
层对它自己的操作Presenter
不需要依赖UIKit
使MVP 模式更加具有可测试性,独立性。
Presenter
@interface MainPresenter () <
NewsModelDelegate
>
/// NewsModel
@property (nonatomic, strong) NewsModel *newsModel;
/// View
@property (nonatomic, weak) id<MainProtocol> view;
@end
我们先来看.m文件的类扩展,MainPresenter
遵守了Model 的协议,代表了Presenter
成为了Model 中的一个代理,也将借助这里面的代理方法请求网络上数据。同时持有一个遵守MainProtocol
的对象,而不需要关心这个对象是谁。
随后我们再来看看Presenter 的.h 文件
@interface MainPresenter : NSObject
/// 初始化View
- (instancetype)initWithView:(id)view;
/// 请求最新信息
- (void)fetchLatestNewsData;
/// 请求过往新闻列表
- (void)fetchBeforeNewsData:(NSString *)date;
@end
其中,initWithView:(id)view
方法就是绑定View,fetchLatestNewsData
和fetchBeforeNewsData:(NSString *)date
两个方法为View 提供了网络请求的接口。
前面如果有一知半解的情况,那么往下看Presenter 的.m 文件,涉及到Presenter 与Model,Presenter 与View 的交互的具体实现,应该就会感知到一点MVP 模式的核心。
@implementation MainPresenter
#pragma mark - Life cycle
/// 初始化View
- (instancetype)initWithView:(id)view {
self = [super init];
if (self) {
// 绑定View
self.view = view;
// 绑定Model
self.newsModel = [[NewsModel alloc] init];
self.newsModel.delegate = self;
}
return self;
}
#pragma mark - Method
/// 请求最新信息
- (void)fetchLatestNewsData {
[self.newsModel fetchLatestNews];
}
/// 请求过往列表
- (void)fetchBeforeNewsData:(NSString *)date {
[self.newsModel fetchBeforeNewsWithDate:date];
}
从上面的代码可以看到,我们绑定了View,Model,并且成为了Model 的代理,那么在fetchLatestNewsData
和fetchBeforeNewsData:(NSString *)date
方法中,Presenter 与Model 进行了交互,在上面介绍Model 的时候,我们也提到了Model 里面的fetchLatestNews
和fetchBeforeNewsWithDate:
方法,在这两个方法中,我们请求到了网络数据,并且交给了Model 的代理,也就是传回给了Presenter。
#pragma mark - Delegate
// MARK: <NewsModelDelegate>
/// 请求Latest 成功的回调
- (void)didReceiveLatestNews:(DayModel *)latestDayModel {
// 在这里Model成功传回了网络信息,Presenter 应该把信息处理成View 可以直接用的样子,传递给View
[self.view showLatestNews:(处理好的信息)];
}
/// 请求before 成功的回调
- (void)didReceiveBeforeNews:(DayModel *)beforeDayModel {
// 在这里Model成功传回了网络信息,Presenter 应该把信息处理成View 可以直接用的样子,传递给View
[self.view showBeforeNews:(处理好的信息)];
}
在上面的两个代理方法中,Model成功传回了网络信息,Presenter 应该把信息处理成View 可以直接用的样子,传递给View。我们说过,View 和Model 应该是完全独立的,更标准的做法,我们不应该直接传递Model 交给View,如果要传递的内容参数比较多,我们可以专门设置一个类来存储处理好的信息:
我们新建了一个NewsData类,里面存放已经处理过的信息,方便让View 直接用:
@interface NewsData : NSObject
@property (nonatomic, copy) NSString *imageURL;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *hint;
@property (nonatomic, copy) NSString *date;
@property (nonatomic, copy) NSString *idStr;
@end
建好后,我们回到Presnter的处理Model 传回信息的方法中:
#pragma mark - Delegate
// MARK: <NewsModelDelegate>
/// 请求Latest 成功的回调
- (void)didReceiveLatestNews:(DayModel *)latestDayModel {
// 在这里Model成功传回了网络信息,Presenter 应该把信息处理成View 可以直接用的样子,传递给View
// 列表数据
NSMutableArray *listMa = [NSMutableArray array];
for (DataModel *model in latestDayModel.stories) {
NewsData *data = [[NewsData alloc] init];
data.title = model.title;
data.hint = model.hint;
data.imageURL = model.imageURL;
data.date = latestDayModel.date;
data.idStr = model.ID;
[listMa addObject:data];
}
NSArray *listData = [NSArray array];
listData = listMa;
NSDictionary *dict = @{@"listData": listData};
[self.view showLatestNews:dict];
}
上面的DataModel
是Model 层的类,也就是传递给Presenter 的原始数据模型,我们要做的就是把原始的DataModel
转化成View 需要的NewsData,之所以用dict
来储存,是因为在知乎日报的项目中,didReceiveLatestNews
方法传回来的不只有列表的数据,还有Banner 的数据,但是这里的代码示范中只是简单展示了一种,在完整的Demo中,dict
不但有listData
,还应该有bannerData
。最后,Presenter 把dict 数据交给了View 展示。
上面就是Presenter 层的所有主要代码,可以看到,Presenter 承担起了MVC 模式中的业务代码,现在的VC 只要拿到数据,就可以直接展示了。因此,Presenter 的代码量也不会非常多。
View
业务代码交给了Presenter,View 就可以专注展示界面和处理用户的交互行为了。
// Presenter
#import "MainPresenter.h"
// Protocol
#import "MainProtocol.h"
#import "NewsData.h"
@interface MainVC () <
MainProtocol,
UITableViewDataSource,
UITableViewDelegate
>
/// Presenter
@property (nonatomic, strong) MainPresenter *presenter;
/// 新闻列表
@property (nonatomic, strong) UITableView *tableView;
/// 列表新闻数据
@property (nonatomic, strong) NSMutableArray *newsList;
@end
@implementation MainVC
- (void)viewDidLoad {
[super viewDidLoad];
self.newsList = [NSMutableArray array];
// 请求最新信息
[self.presenter fetchLatestNewsData];
// list
[self.view addSubview:self.tableView];
self.tableView.dataSource = self;
self.tableView.delegate = self;
}
// MARK: <MainProtocol>
/// 展示最新新闻
- (void)showLatestNews:(NSDictionary *)latestModel {
[self.newsList removeAllObjects];
[self.newsList addObject:latestModel[@"listData"]];
[self.tableView reloadData];
}
// MARK: <UITableViewDataSource>
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.newsList.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.newsList.count == 0 ? 0 : 6;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MainTableViewCell *cell = [MainTableViewCell creatCellDefault:tableView];
if (self.newsList.count != 0) {
[cell setNormalBackground];
}
NewsData *newsData = self.newsList[indexPath.section][indexPath.row];
cell.titleLab.text = newsData.title;
cell.hintLab.text = newsData.hint;
[cell.imgView setImageWithURL:[NSURL URLWithString:newsData.imageURL] placeholderImage:[UIImage imageNamed:@"defaultImage"]];
return cell;
}
#pragma mark - Getter
- (MainPresenter *)presenter {
if (_presenter == nil) {
_presenter = [[MainPresenter alloc] initWithView:self];
}
return _presenter;
}
我们可以看到,View 绑定了Presenter,也遵循了MainProtocol
协议,因此,当Presenter 和Model 交互,请求到网络数据后,可以通过MainProtocol
中的showLatestNews:(NSDictionary *)latestModel
传递给View,View拿到数据后,就可以进行一系列在UI层面上的展示。
在上面的demo 中,我们拿到数据后,储存在了newsList
里面,从而在tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
方法中展示到tableView 中。
以上是比较简单的用代码示范MVP 模式中各个部分的工作介绍。项目Demo 有项目的完整代码。
4 其他的业务处理
按钮点击事件
我们刚刚说过,MVP 模式中,View 层要处理用户交互事件,但是如果涉及到与Model 的交互,应该交由Presenter 处理,所以,要处理按钮的点击事件,应该先判断按钮点击事件是否涉及到数据的改变。
举个栗子,如果按钮点击后只是造成了例如界面背景颜色的改变,或者一个动画效果,而不涉及到了数据的改变, 那么View 层不需要通知到Presenter 来处理业务,可以直接在View 层中处理该事件。
但是如果如果按钮点击后造成了例如点赞数,收藏数加一或者减一这种需要涉及到数据的改变或者需要触发其他的业务逻辑,那么最好还是通知到Presenter 层来处理,以保持MVP 模式的分层结构。
界面跳转
根据MVP 模式的设计思路,Presenter 应该只关心业务逻辑的实现,不直接操作View 和进行界面跳转等视图层操作。因此,当需要进行界面跳转时,Presenter 应该将跳转的请求传递给View,由View 负责进行具体的跳转操作。
在这个过程中,Presenter 只需要关心跳转请求的结果是否符合业务逻辑即可。
而跳转后的界面对应的Presenter 可以在界面初始化时由View创建并绑定,与之前的Presenter 进行解绑。所以,界面间通信应该是Presenter 与View 的通信,而不是Presenter 与Presenter 的通信。
还是举知乎日报项目的例子:
点击新闻列表的任意一个cell 进入新闻详情页,这个过程中,需要传入一个新闻的id 号才能进行新闻详情的请求,那么我们可以想到,是消息列表界面的View 处理了点击事件,把新闻的ID 号传给了新闻详情页模块的Presenter,再由Presenter 进行网络请求,拿到新闻详情页的数据给View 展示。
@implementation MainVC
// 点击新闻,进入详情页
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NewsData *newsData = self.newsList[indexPath.section][indexPath.row];
NSString *idStr = newsData.idStr;
NewDetailsPresenter *presenter = [[NewDetailsPresenter alloc] initWithID:idStr];
NewDetailsVC *newDetailsVC = [[NewDetailsVC alloc] initWithPresenter:presenter];
[self.navigationController pushViewController:newDetailsVC animated:YES];
}
上面的NewDetailsPresenter
就是新闻详情页模块的Presenter,NewDetailsVC
就是新闻详情页模块的View。
在NewDetailsPresenter
中:
@implementation NewDetailsPresenter
/// 初始化View
- (instancetype)initWithID:(NSString *)idStr {
self = [super init];
if (self) {
self.idStr = idStr;
self.model = [[NewDetailsModel alloc] init];
self.model.delegate = self;
}
return self;
}
NewDetailsModel
自然也是新闻详情页模块的Model,设置代理的操作和首页一样。
在NewDetailsVC
中,与Presenter 是这样进行绑定的:
@implementation NewDetailsVC
- (instancetype)initWithPresenter:(NewDetailsPresenter *)presenter {
self = [super init];
if (self) {
self.presenter = presenter;
self.presenter.view = self;
}
return self;
}
其他的交互操作,数据传递,和之前提到的首页新闻列表类似。
上面的代码也可以看到,Presenter应该只关心业务逻辑的实现,不直接操作View和进行界面跳转等视图层操作。
5 总结
MVP 的优点:
- 代码更加模块化,易于维护和扩展。
- 视图和模型之间的交互逻辑通过Presenter 进行协调,逻辑更加清晰。
- Presenter 可以通过接口来实现,使得测试更加容易。
但同时,MVP 模式也有不尽人意的地方:
- Presenter 层会增加代码量,增加了开发时间和成本。
- 对于小型应用程序来说,MVP 模式可能会显得过于繁琐。
- Presenter 和View 之间的接口设计可能会变得复杂,需要额外的注意。