iOS架构模式之MVP

2019-03-18  本文已影响0人  _森宇_

前言

软件开发中,最常见的设计模式是Model-View-Controller(MVC),MVC也是构建Cocoa应用程序的标准模板,MVC设计模式为应用程序中的对象分配三个角色之一:模型,视图或控制器。采用这种模式的好处很多,这些应用程序中的许多对象往往更具可重用性,比其他应用程序更容易扩展。Apple对于Cocoa的MVC框架各角色之间的关系定义如下:

AppleMVC.png

但随着时间的推移,开发的业务不断增多,MVC暴露出了越来越多的问题,随之而衍生出MVPMVVMVIPER等更高级架构。本文会先总结MVC在iOS应用开发中的存在的问题,再对MVP设计模式进行客观性描述。

MVC架构的缺陷

  1. 厚重的Controller难以维护。

    Controller 层是app的中枢机构,协调模型和视图之间的所有交互,其不仅管理所拥有的视图的视图层次结构,还要响应视图的用户交互操作等等,同时往往也会充满Model层模型逻辑以及View层业务逻辑等等的“胶水代码”。 厚重的Controller 正是由于大量的代码被放进 UIViewController,导致他们变的相当臃肿,一个 UIViewController里的代码成千上万行的事并不是前所未见的。

    对于厚重的Controller,由于其庞大的规模往往很难维护;包含几十个属性,使他们的状态难以管理;遵循许多协议,导致协议的响应代码和Controller的逻辑代码混淆在一起。行业中对这种控制器有个专业词汇Massive ViewControler(臃肿的视图控制器)。

  2. 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. ...

  1. 较差的可测试性。

    这一点一方面是因为Cocoa框架里的Controller层,就是我们最熟悉的UIViewControllerUIView是天然耦合的,很多UIView的生命周期方法都存在于UIViewController,另一方面我们很多时候也习惯于把UI操作甚至初始化操作放在UIViewController里,导致UI和业务逻辑混杂在一起。当你想对业务逻辑编写单元测试的时候,分离这些成分的单元测试成了一个艰巨的任务,因此大多数人选择忽略这个任务,那就是不做任何测试。

ControllerView 很难做到相互独立。虽然你可以把控制器里的一些业务逻辑和数据转换的工作交给 Model,但是你再想把负担往 View 里面分摊的时候就没办法了;因为 View 的主要职责就只是将用户的操作行为交给 Controller 去处理而已。于是 ViewController 最终就变成了所有东西的代理和数据源,甚至还负责网络请求的发起和取消。MVC的架构变成了:

Real MVC

MVC的优化方案之MVP

Model-View-Presenter(MVP)是MVC体系结构模式的一种变体,其主要目的是将放在Controller里面的业务逻辑抽离出来,让UIViewControllerUIView整合成View层,只负责页面布局和交互相关功能,从而减轻UIViewController的负担,并有利于对业务逻辑功能进行单元测试。

MVP在1996年就已经被提出,发展到现在已经出现好多变种,这里提供一种目前比较多人使用的规范:

根据以上规范,不同层级关系图如下:

MVP

MVP带来的便利

MVP的缺点

由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了

在iOS工程中使用MVP架构

接下来会用一个简单的例子说明在iOS实际开发中如何使用MVP进行设计。例子是一个模拟获取用户数据显示到列表的demo。下面是详细设计过程:

首先定义用户信息的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

首先创建一个能让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持有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
上一篇 下一篇

猜你喜欢

热点阅读