前言
软件开发中,最常见的设计模式是Model-View-Controller
(MVC),MVC
也是构建Cocoa
应用程序的标准模板,MVC
设计模式为应用程序中的对象分配三个角色之一:模型,视图或控制器。采用这种模式的好处很多,这些应用程序中的许多对象往往更具可重用性,比其他应用程序更容易扩展。Apple
对于Cocoa
的MVC框架各角色之间的关系定义如下:
但随着时间的推移,开发的业务不断增多,MVC
暴露出了越来越多的问题,随之而衍生出MVP
、MVVM
、VIPER
等更高级架构。本文会先总结MVC
在iOS应用开发中的存在的问题,再对MVP
设计模式进行客观性描述。
MVC架构的缺陷
-
厚重的Controller难以维护。
Controller
层是app的中枢机构,协调模型和视图之间的所有交互,其不仅管理所拥有的视图的视图层次结构,还要响应视图的用户交互操作等等,同时往往也会充满Model
层模型逻辑以及View
层业务逻辑等等的“胶水代码”。 厚重的Controller 正是由于大量的代码被放进UIViewController
,导致他们变的相当臃肿,一个UIViewController
里的代码成千上万行的事并不是前所未见的。对于厚重的
Controller
,由于其庞大的规模往往很难维护;包含几十个属性,使他们的状态难以管理;遵循许多协议,导致协议的响应代码和Controller
的逻辑代码混淆在一起。行业中对这种控制器有个专业词汇Massive ViewControler
(臃肿的视图控制器)。 -
Model层过于单薄。
我们通常会在
Model
层定义数据成员属性,由于无需再手动管理释放变量,Model层既没有对象的构造,也没有复杂的业务处理,implementation
基本上都是空的。然而根据 Apple MVC
文档的描述,Model
应包括操作和处理该数据的逻辑和计算,其业务逻辑不应被拖入到Controller
。
Model objects encapsulate the data specific to an application and define the logic and computation that manipulate and process that data. ...
-
较差的可测试性。
这一点一方面是因为
Cocoa
框架里的Controller
层,就是我们最熟悉的UIViewController
和UIView
是天然耦合的,很多UIView
的生命周期方法都存在于UIViewController
,另一方面我们很多时候也习惯于把UI操作甚至初始化操作放在UIViewController
里,导致UI和业务逻辑混杂在一起。当你想对业务逻辑编写单元测试的时候,分离这些成分的单元测试成了一个艰巨的任务,因此大多数人选择忽略这个任务,那就是不做任何测试。
Controller
和 View
很难做到相互独立。虽然你可以把控制器里的一些业务逻辑和数据转换的工作交给 Model
,但是你再想把负担往 View
里面分摊的时候就没办法了;因为 View
的主要职责就只是将用户的操作行为交给 Controller
去处理而已。于是 ViewController
最终就变成了所有东西的代理和数据源,甚至还负责网络请求的发起和取消。MVC的架构变成了:
MVC的优化方案之MVP
Model-View-Presenter
(MVP)是MVC
体系结构模式的一种变体,其主要目的是将放在Controller
里面的业务逻辑抽离出来,让UIViewController
和UIView
整合成View
层,只负责页面布局和交互相关功能,从而减轻UIViewController
的负担,并有利于对业务逻辑功能进行单元测试。
- Model层:数据层,或者负责处理数据的 数据接口层。比如 Person 和 PersonDataProvider 类
- View层:展示层(GUI)。对于 iOS 来说所有以 UI 开头的类基本都属于这层。
- Presenter层:作为中间人,协调其他层之间的逻辑。一般来说,当用户对 View 有操作时它负责去修改相应 Model;当 Model 的值发生变化时它负责去更新对应 View。
MVP在1996年就已经被提出,发展到现在已经出现好多变种,这里提供一种目前比较多人使用的规范:
-
View
层是由UIViewController
和UIView
共同组成; -
View
层将委托Presenter
层对它自己的操作; -
Presenter
层拥有对View
层交互的逻辑; -
Presenter
层跟Model
层通信,并将数据转化成对适应UI的数据并更新View
; -
Presenter
不需要依赖UIKit
; -
View
层是单一,因为它是被动接受命令,没有主动能力。
根据以上规范,不同层级关系图如下:
MVPMVP带来的便利
- 职责划分清晰 — 业务逻辑划分到Presenter和Model,View只负责页面交互,从而降低耦合度。
- 可测性强 — 基于一个功能简单的View层,可以测试大多数业务逻辑。
- 复用性 — Presenter和View之间通过抽象方法交互,同样的业务逻辑可以很方便复用。
MVP的缺点
由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了
在iOS工程中使用MVP架构
接下来会用一个简单的例子说明在iOS实际开发中如何使用MVP
进行设计。例子是一个模拟获取用户数据显示到列表的demo。下面是详细设计过程:
- Model层
首先定义用户信息的Model
;
@interface User : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong) NSString *email;
@property (nonatomic, assign) int age;
// 类方法构造对象
+ (User *)userWithFirstName:(NSString *)firstName
lastName:(NSString *)lastName
email:(NSString *)email
age:(int)age;
@end
另外定义一个Service
,专门负责数据处理,在这里模拟请求返回数据;
@implementation UserService
- (void)getUsers:(void(^)(NSArray<User *> *users))handler {
User *user1 = [User userWithFirstName:@"First1" lastName:@"Last1" email:@"Iyad@test.com" age:36];
User *user2 = [User userWithFirstName:@"First2" lastName:@"Last2" email:@"Mila@test.com" age:24];
// 模拟网络请求耗时
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
handler(@[user1, user2]);
});
}
@end
- Presenter层
首先创建一个能让View
直接使用的数据模型,其包含View
需要的所有信息。
@interface UserViewData : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@end
接下来是View
的抽象方法,Presenter
可以不用知道是哪个ViewController
,就可以直接调用其方法,在这里使用protocol
方法;
@protocol UserView <NSObject>
- (void)startLoading;
- (void)finishLoading;
- (void)setUsers:(NSArray<UserViewData *> *)users;
- (void)setEmptyUsers;
@end
协议中的方法会在View
中实现,Presenter
会调用这些方法来更新界面。
@interface UserPresenter()
@property (nonatomic, strong) UserService *userService;
@property (nonatomic, weak) id<UserView> userView;
@end
@implementation UserPresenter
- (instancetype)initWithUserService:(UserService *)userService {
if (self = [super init]) {
self.userService = userService;
}
return self;
}
- (void)attachView:(id<UserView>)view {
_userView = view;
}
- (void)detachView {
_userView = nil;
}
- (void)getUsers {
[_userView startLoading];
__weak typeof(self) weakSelf = self;
[_userService getUsers:^(NSArray<User *> *users){
[weakSelf.userView finishLoading];
if (users.count == 0) {
[weakSelf.userView setEmptyUsers];
} else {
NSMutableArray *userArray = [NSMutableArray array];
for(User *user in users) {
UserViewData *userData = [[UserViewData alloc] init];
userData.name = [NSString stringWithFormat:@"%@ %@", user.firstName, user.lastName];
userData.age = [NSString stringWithFormat:@"%d years", user.age];
[userArray addObject:userData];
}
[weakSelf.userView setUsers:userArray];
}
}];
}
@end
Presenter通过attachView:
方法绑定视图,并实现业务逻辑,在数据更新时调用协议方法更新界面;Presenter内还包含关于User数据模型转换成视图能用的UserViewData
格式的工作。
- View层
View
持有Presenter
对象,实现Presenter
页面交互协议方法,并在初始化时绑定到,用户交互时调用Presenter
更新数据;
@interface ViewController()<UITableViewDataSource, UserView>
@property (weak, nonatomic) IBOutlet UIView *emptyView;
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator;
@property (nonatomic, strong) UserPresenter *userPresenter;
@property (nonatomic, strong) NSArray *usersToDisplay;
@end
@implementation ViewController
#pragma mark - life cycles
- (void)viewDidLoad {
[super viewDidLoad];
_tableView.dataSource = self;
_activityIndicator.hidesWhenStopped = YES;
// 绑定视图
[self.userPresenter attachView:self];
// 获取用户数据
[self.userPresenter getUsers];
}
#pragma mark - UserView
- (void)startLoading {
[_activityIndicator startAnimating];
}
- (void)finishLoading {
[_activityIndicator stopAnimating];
}
- (void)setUsers:(NSArray<UserViewData *> *)users {
_usersToDisplay = users;
_tableView.hidden = NO;
_emptyView.hidden = YES;
[_tableView reloadData];
}
- (void)setEmptyUsers {
_tableView.hidden = YES;
_tableView.hidden = NO;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.usersToDisplay.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"UserCell"];
UserViewData *user = [self.usersToDisplay objectAtIndex:indexPath.row];
cell.textLabel.text = user.name;
cell.detailTextLabel.text = user.age;
return cell;
}
#pragma mark - getters and setters
- (UserPresenter *)userPresenter {
if (_userPresenter == nil) {
UserService *userService = [[UserService alloc] init];
_userPresenter = [[UserPresenter alloc] initWithUserService:userService];
}
return _userPresenter;
}
- (NSArray *)usersToDisplay {
if (_usersToDisplay == nil) {
_usersToDisplay = [NSArray array];
}
return _usersToDisplay;
}
@end
网友评论