美文网首页
iOS架构模式之MVP

iOS架构模式之MVP

作者: _森宇_ | 来源:发表于2019-03-18 22:10 被阅读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的负担,并有利于对业务逻辑功能进行单元测试。

    • Model层:数据层,或者负责处理数据的 数据接口层。比如 PersonPersonDataProvider
    • View层:展示层(GUI)。对于 iOS 来说所有以 UI 开头的类基本都属于这层。
    • Presenter层:作为中间人,协调其他层之间的逻辑。一般来说,当用户对 View 有操作时它负责去修改相应 Model;当 Model 的值发生变化时它负责去更新对应 View。

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

    • View层是由UIViewControllerUIView共同组成;
    • View层将委托Presenter层对它自己的操作;
    • Presenter层拥有对View层交互的逻辑;
    • Presenter层跟Model层通信,并将数据转化成对适应UI的数据并更新View
    • Presenter不需要依赖UIKit
    • View层是单一,因为它是被动接受命令,没有主动能力。

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

    MVP

    MVP带来的便利

    • 职责划分清晰 — 业务逻辑划分到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
    

    相关文章

      网友评论

          本文标题:iOS架构模式之MVP

          本文链接:https://www.haomeiwen.com/subject/foavmqtx.html