聊聊基于MVP 模式下的软件设计
概论
MVC的缺点在于并没有区分业务逻辑和业务展示, 这对单元测试很不友好,不光不利于单元测试而且不利于代码的阅读和维护,眉毛胡子一把抓是后续难以维护的症结所在。
MVP针对以上缺点做了优化, 它将业务逻辑和业务展示也做了一层隔离, 对应的就变成了MVCP。M和V功能不变, 原来的C现在只负责布局, 而所有的业务逻辑全都转移到了P层。P层处理完了业务逻辑,如果要更改view的显示,那么可以通过代理来实现,这样可以减轻耦合,同时可以单独测试P层的业务逻辑。
我们来看一下MVP模式能否解决MVC模式存在的问题
【1】Controller层职责过多,Model和View层太简单
在MVP模式下,Controller层和View层已经合并为View层,专门负责处理UI更新和事件传递,Model层还是作为实体类。
原本写在ViewController层的业务逻辑已经迁移到Presenter中。MVP模式较好地解决了Controller层职责过多的问题。
【2】业务逻辑和UI混杂在一起,难以编写单元测试
Presenter层主要处理业务逻辑,ViewController层实现Presenter提供的接口,
Presenter通过接口去更新View,这样就实现了业务逻辑和UI解耦。如果我们要编写单元测试的话,
只需要Mock一个对象实现Presenter提供的接口就好了。MVP模式较好地解决了UI和逻辑的解耦。
【3】业务逻辑代码大量存在于Controller层,维护困难
通过把业务逻辑迁移到Presenter层,Controller层的困境似乎得到了解决,但是如果某个需求逻辑较为复杂,
单纯的把业务逻辑迁移解决不了根本的问题,Presenter层也会存在大量业务逻辑代码,维护困难。
这个问题可以通过类别扩展或者通过面向接口编程的方式实现代码的分散管理。
通信方式
- 1. 当视图接收到来自用户的事件时,会将事件转交给 Presenter 进行处理;
- 2. 被动的视图实现presentr的代理,当需要更新视图时 Presenter回调代理来更新视图的内容,这样让presenter专注于业务逻辑,view专注于显示逻辑。
- 3. Presenter 负责对模型进行操作和更新,在需要时取出其中存储的信息;
- 4. 当模型层改变时,可以将改变的信息发送给观察者 Presenter;
一个点赞功能的例子
MVC下的的点赞功能
blogViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
BlogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.title = cellHelper.blogTitleText;
cell.summary = cellHelper.blogSummaryText;
cell.likeState = cellHelper.isLiked;
cell.likeCountText = cellHelper.blogLikeCountText;
cell.shareCountText = cellHelper.blogShareCountText;
//点赞的业务逻辑
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已经赞过它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//点赞的业务展示
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
return cell;
}
===========================================
BlogViewCell.m
- (IBAction)onClickLikeButton:(UIButton *)sender {
!self.didLikeHandler ?: self.didLikeHandler();
}
#pragma mark - Interface
- (void)setTitle:(NSString *)title {
self.titleLabel.text = title;
}
- (void)setSummary:(NSString *)summary {
self.summaryLabel.text = summary;
}
- (void)setLikeState:(BOOL)isLiked {
[self.likeButton setTitleColor:isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)setLikeCountText:(NSString *)likeCountText {
[self.likeButton setTitle:likeCountText forState:UIControlStateNormal];
}
- (void)setShareCountText:(NSString *)shareCountText {
[self.shareButton setTitle:shareCountText forState:UIControlStateNormal];
}
MVP下的点赞代码
blogViewController.m
//点赞事件
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.presenter = self.presenter.allDatas[indexPath.row];//PV绑定
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
[weakCell.presenter likeBlogWithCompletionHandler:^(NSError *error, id result) {
!error ?: [weakCell showToastWithText:error.domain];
}];
}];
return cell;
}
==========================================
BlogCellPresenter.m
- (void)likeBlogWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
if (self.blog.isLiked) {
!completionHandler ?: completionHandler([NSError errorWithDomain:@"你已经赞过了哦~" code:123 userInfo:nil], nil);
} else {
BOOL response = [self.view respondsToSelector:@selector(blogPresenterDidUpdateLikeState:)];
self.blog.isLiked = YES;
self.blog.likeCount += 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
[[UserAPIManager new] likeBlogWithBlogId:self.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
self.blog.isLiked = NO;
self.blog.likeCount -= 1;
!response ?: [self.view blogPresenterDidUpdateLikeState:self];
}
!completionHandler ?: completionHandler(error, result);
}];
}
}
==========================================
BlogViewCell.m
#pragma mark - BlogCellPresenterCallBack
- (void)blogPresenterDidUpdateLikeState:(BlogCellPresenter *)presenter {
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.likeButton setTitleColor:presenter.isLiked ? [UIColor redColor] : [UIColor blackColor] forState:UIControlStateNormal];
}
- (void)blogPresenterDidUpdateShareState:(BlogCellPresenter *)presenter {
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}
#pragma mark - Action
- (IBAction)onClickLikeButton:(UIButton *)sender {
!self.didLikeHandler ?: self.didLikeHandler();
}
#pragma mark - Setter
- (void)setPresenter:(BlogCellPresenter *)presenter {
_presenter = presenter;
presenter.view = self;
self.titleLabel.text = presenter.blogTitleText;
self.summaryLabel.text = presenter.blogSummaryText;
self.likeButton.selected = presenter.isLiked;
[self.likeButton setTitle:presenter.blogLikeCountText forState:UIControlStateNormal];
[self.shareButton setTitle:presenter.blogShareCountText forState:UIControlStateNormal];
}
从上面的代码对比可以看出来,MVP的代码量比MVC多出来一部分,但是MVP在层次上更加清晰,业务逻辑和业务展示彻底分离,让presenter和view可以单独测试,而MVC则把这两者混在一起,没法单独测试。
另一个例子
//LoginModel.h
#import <Foundation/Foundation.h>
#import "HttpUtils.h"
//M层(数据层,数据库,网络,文件等...)
@interface LoginModel : NSObject
//业务方法
- (void)loginWithName:(NSString*)name pwd:(NSString*)pwd callback:(Callback)callback;
@end
//LoginModel.m
#import "LoginModel.h"
@implementation LoginModel
- (void)loginWithName:(NSString*)name pwd:(NSString*)pwd callback:(Callback)callback{
//实现功能
//例如:访问网络?访问数据库?
//数据曾划分了模块()
[HttpUtils postWithName:name pwd:pwd callback:^(NSString *result) {
//解析json ,xml数据
//然后保存数据库
//中间省略100行代码
callback(result);//返回数据回调
}];
}
@end
//接口协议层
#import <Foundation/Foundation.h>
@protocol LoginView <NSObject>
- (void)onLoginResult:(NSString*)result;
@end
#import <Foundation/Foundation.h>
#import "LoginView.h"
#import "LoginModel.h"
//中介(用于关联M层和V层)
@interface LoginPresenter : NSObject
//提供一个业务方法
- (void)loginWithName:(NSString*)name pwd:(NSString*)pwd;
- (void)attachView:(id<LoginView>)loginView;
- (void)detachView;
@end
#import "LoginPresenter.h"
//P是中介(职责是用于关联M和V)
//P层需要:持有M层的引用和V层的引用(OOP)思想
@interface LoginPresenter ()
@property (nonatomic,strong) LoginModel *loginModel;
@property (nonatomic,strong) id<LoginView> loginView;
@end
@implementation LoginPresenter
- (instancetype)init{
self = [super init];
if (self) {
//持有M层的引用
_loginModel = [[LoginModel alloc]init];
}
return self;
}
//提供绑定V层方法
//绑定
- (void)attachView:(id<LoginView>)loginView{
_loginView = loginView;
}
//解除绑定
- (void)detachView{
_loginView = nil;
}
//实现业务方法
- (void)loginWithName:(NSString*)name pwd:(NSString*)pwd{
[_loginModel loginWithName:name pwd:pwd callback:^(NSString *result) {
if (_loginView != nil) {
[_loginView onLoginResult:result];
}
}];
}
#import "ViewController.h"
#import "LoginView.h"
#import "LoginPresenter.h"
@interface ViewController ()<LoginView>
@property (nonatomic,strong) LoginPresenter* presenter;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_presenter = [[LoginPresenter alloc ]init];
[_presenter attachView:self];
//程序一旦运行立马执行请求(测试)(按钮或者事件)
[_presenter loginWithName:@"18842693828" pwd:@"123456"];
}
- (void)onLoginResult:(NSString *)result{
NSLog(@"返回结果%@",result);
}
上述是一个MVP模式下的登录功能的全部代码,我们可以看到网络请求是放在 M层的这点和 MVVM中是不一样的,基本可以看到是通过 V(View & ViewController)持有 P,并通过 V实现 协议接口达到 P 向 V 通信的。
总结:
与 MVVM 的瘦Model相比 MVP下的算是胖Model了。
可以看到在MVP里面业务逻辑和业务展示是分在不同的地方实现,那么就可以分开测试二者了,而不想MVC那样想测试下业务逻辑,还必须生成一个view,这不合理,因为业务逻辑改变的model的数据,和view无关。
MVP相对于MVC, 它其实只做了一件事情, 即分割业务展示和业务逻辑. 展示和逻辑分开后, 只要我们能保证V在收到P的数据更新通知后能正常刷新页面, 那么整个业务就没有问题. 因为V收到的通知其实都是来自于P层的数据获取/更新操作, 所以我们只要保证P层的这些操作都是正常的就可以了. 即我们只用测试P层的逻辑, 不必关心V层的情况。
参考文章: