美文网首页
设计模式之 MVC 和 MVVM

设计模式之 MVC 和 MVVM

作者: _既白_ | 来源:发表于2019-03-27 00:45 被阅读0次

    MVC

    MVC

    MVC 设计模式是“模型-视图-控制器”。该模式不仅定义了对象在应用程序中扮演的角色, 还定义了对象之间的通信方式。Model 用于保存数据, View 向用户提供交互式界面, View Controller 调解模型和 View 之间的交互。

    MVC 是苹果官方推荐使用的设计模式,苹果许多 Cocoa 技术和体系框架都基于 MVC 。相对优点:对象易于重用,接口定义简单,程序易于扩展。

    Model 模型

    iOS 中的 Model 层不应该是一个单纯的数据结构,它应该起到发出 HTTP 请求、进行字段验证以及持久存储的职责,同时为上层提供网络请求的方法以及字段作为接口,为视图的展示提供数据源的作用。我们应该将更多的与 Model 层有关的业务逻辑移到 Model 中以控制 Controller 的复杂性。

    Model 用于封装应用程序的数据, 并定义操作和处理该数据的逻辑和计算。理想情况下, 模型对象不应显式连接到显示其数据并允许用户编辑该数据的视图对象, 它不应涉及用户界面和表示问题。

    通信: 视图层中创建或修改数据的用户操作通过View Controller进行通信, 并创建或更新Model。当Model更改 (例如, 通过网络连接接收新数据) 时, 它会通知View Controller, 该对象将更新相应的View

    View 视图

    View (通常) 是 UIKit 组件或程序定义的 uikit 组件集合。主要负责视图绘制,渲染,响应用户操作。View的一个主要作用是显示应用程序的 Model 中的数据, 并且可以对Model 中的数据进行编辑。视图中永远不应该有对模型的直接引用, 并且应该只有通过 IBAction 事件对控制器的引用。

    通信: 通过应用程序的View Controller查看对象了解模型数据中的更改, 并通过View Controller 将用户在视图层的更改 (例如, 在文本字段中输入的文本) 传达给应用程序的Model

    View Controller 控制器

    总体来说,Controller 层要负责以下的问题(包括但不仅限于):

    • 管理根视图的生命周期和应用生命周期
    • 负责将视图层的 UIView 对象添加到持有的根视图上;
    • 负责处理用户行为,比如 UIButton 的点击以及手势的触发;
    • 储存当前界面的状态;
    • 处理界面之间的跳转;
    • 作为 UITableView 以及其它容器视图的代理以及数据源;
    • 负责 HTTP 请求的发起;

    管理生命周期

    Controller 层作为整个 MVC 架构模式的中枢,承担着非常重要的职责,不仅要与 Model 以及 View 层进行交互,还有通过 AppDelegate 与诸多的应用生命周期打交道。

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions;
    - (void)applicationWillResignActive:(UIApplication *)application;
    - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
    

    虽然与应用生命周期沟通的工作并不在单独的Controller 中,但是 self.window.rootController 作为整个应用程序界面的入口,还是需要在 AppDelegate 中进行设置。

    除此之外,由于每一个 UIViewController 都持有一个视图对象,所以每一个 UIViewController 都需要负责这个根视图的加载、布局以及生命周期的管理,包括:

    - (void)loadView;
    
    - (void)viewWillLayoutSubviews;
    - (void)viewDidLayoutSubviews;
    
    - (void)viewDidLoad;
    - (void)viewWillAppear:(BOOL)animated;
    - (void)viewDidAppear:(BOOL)animated;
    

    除了负责应用生命周期和视图生命周期,控制器还要负责展示内容和布局。

    负责展示内容和布局

    由于每一个UIViewController 都持有一个UIView 的对象,所以视图层的对象想要出现在屏幕上,必须成为这个根视图的子视图,也就是说视图层完全没有办法脱离 UIViewController 而单独存在,其一方面是因为 UIViewController 隐式的承担了应用中路由的工作,处理界面之间的跳转,另一方面就是 UIViewController 的设计导致了所有的视图必须加在其根视图上才能工作。

    处理用户行为

    UIViewController 中处理用户的行为是经常需要做的事情,这部分代码不能放到视图层或者其他地方的原因是,用户的行为经常需要与 Controller 的上下文有联系,比如,界面的跳转需要依赖于 UINavigationController对象:

    - (void)registerButtonTapped:(UIButton *)button {
        RegisterViewController *registerViewController = [[RegisterViewController alloc] init];
        [self.navigationController pushViewController:registerViewController animated:YES];
    }
    

    而有的用户行为需要改变模型层的对象、持久存储数据库中的数据或者发出网络请求,主要因为我们要秉承着 MVC 的设计理念,避免 Model 层和 View 层的直接耦合。

    存储当前界面的状态

    iOS 中,我们经常需要处理表视图,而在现有的大部分表视图在加载内容时都会进行分页,使用下拉刷新和上拉加载的方式获取新的条目,而这就需要在 Controller 层保存当前显示的页数:

    @interface TableViewController ()
    
    @property (nonatomic, assign) NSUInteger currentPage;
    
    @end
    

    只有保存在了当前页数的状态,才能在下次请求网络数据时传入合适的页数,最后获得正确的资源,当然哪怕当前页数是可以计算出来的,比如通过当前的 Model 对象的数和每页个 Model 数,在这种情况下,我们也需要在当前 Controller 中 Model 数组的值。

    @interface TableViewController ()
    
    @property (nonatomic, strong) NSArray<Model *> *models;
    
    @end
    

    在 MVC 的设计中,这种保存当前页面状态的需求是存在的,在很多复杂的页面中,我们也需要维护大量的状态,这也是 Controller 需要承担的重要职责之一。

    处理界面之间的跳转

    由于 Cocoa Touch 提供了 UINavigationControllerUITabBarController 这两种容器 Controller,所以iOS 中界面跳转的这一职责大部分都落到了 Controller 上。

    iOS 中总共有三种界面跳转的方式:

    • UINavigationController 中使用 push 和 pop改变栈顶的UIViewController `对象;
    • UITabBarController 中点击各个 UITabBarItem 实现跳转;
    • 使用所有的 UIViewController 实例都具有的 -presentViewController:animated:completion方法;

    因为所有的 UIViewController 的实例都可以通过 navigationController 这一属性获取到最近的 UINavigationController 对象,所以我们不可避免的要在 Controller 层对界面之间的跳转进行操作。

    作为数据源以及代理

    很多 Cocoa Touch 中视图层都是以代理的形式为外界提供接口的,其中最为典型的例子就是 UITableView 和它的数据源协议 UITableViewDataSource 和代理 UITableViewDelegate

    这是因为 UITableView 作为视图层的对象,需要根据 Model 才能知道自己应该展示什么内容,所以在早期的很多视图层组件都是用了代理的形式,从 Controller 或者其他地方获取需要展示的数据。

    #pragma mark - UITableViewDataSource
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
        return self.models.count;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
        Model *model = self.models[indexPath.row];
        [cell setupWithModel:model];
        return cell;
    }
    

    上面就是使用 UITableView 时经常需要的方法。

    很多文章中都提供了一种用于减少 Controller 层中代理方法数量的技巧,就是使用一个单独的类作为 UITableView 或者其他视图的代理:

    self.tableView.delegate = anotherObject;
    self.tableView.dataSource = anotherObject;
    

    负责 HTTP 请求的发起

    当用户的行为触发一些事件时,比如下拉刷新、更新 Model 的属性等等,Controller 就需要通过 Model 层提供的接口向服务端发出 HTTP 请求,这一过程其实非常简单,但仍然是 Controller 层的职责,也就是响应用户事件,并且更新 Model 层的数据。

    - (void)registerButtonTapped:(UIButton *)button {
        LoginManager *manager = [LoginManager manager];
        manager.countryCode = _registerPanelView.countryCode;
        ...
        [manager startWithSuccessHandler:^(CCStudent *user) {
            self.currentUser = user;
            ...
        } failureHandler:^(NSError *error) {
            ...
        }];
    }
    

    当按钮被点击时 LoginManager 就会执行 -startWithSuccessHandler:failureHandler: 方法发起请求,并在请求结束后执行回调,更新 Model的数据。

    iOS 中 Controller 层的职责一直都逃不开与 View 层和 Model 层的交互,因为其作用就是视图层的用户行为进行处理并更新视图的内容,同时也会改变模型层中的数据、使用 HTTP 请求向服务端请求新的数据等作用,其功能就是处理整个应用中的业务逻辑和规则。但是由于 iOS 中 Controller 的众多职责,单一的 UIViewController 类可能会有上千行的代码,使得非常难以管理和维护,我们也希望在 iOS 中引入新的架构模式来改变 Controller 过于臃肿这一现状。

    Controller 总结

    View Controller 充当应用程序的一个或多个 View 及其一个或多个 Model 之间的中介。因此, View Controller 是查看对象了解Model 变化的管道, 反之亦然。View Controller 负责执行应用程序的设置和协调任务, 并管理其他对象的生命周期。

    通信: View Controller 是用来解析在 View 中执行的用户操作, 并将新的或更改的数据传达给模型层。当Model更改时, View Controller将新模型数据传达给View, 以便它们可以显示它。

    MVC 的弱点

    Massive View Controller (臃肿的控制器)

    由于视图控制器中放置的代码数量非常多, 它们往往会变得相当膨胀。在 iOS 中, 视图控制器延伸到数千行代码并非闻所未闻。臃肿的视图控制器很难维护 (因为它们的大小非常大), 甚至包含几十个属性, 使它们的状态难以管理, 并且混合了许多事件响应,协议代理,通知,网络请求等代码逻辑。

    无论是手动还是单元测试, 都很难测试大型视图控制器, 因为它们有很多状态。

    Missing Network Logic (缺少网络逻辑)

    MVC 所有对象都可以分为模型、视图或控制器。那么我们应该把网络代码放在哪里呢?与 API 进行通信的代码在哪里进行实现呢?

    第一,如果将网络代码放在模型对象(Model)中, 可能带来的危险:网络调用应该以异步方式完成, 因此, 如果网络请求的寿命超过拥有它的模型的寿命,该怎么办呢?
    第二,你绝对不应该把网络代码放在视图(View)中。
    第三,把网络代码放在控制器(View Controller),会使得控制器更加臃肿。

    Poor Testability (测试性差)

    MVC 的另一个大问题是它不鼓励开发人员编写单元测试。由于视图控制器将视图操作逻辑与业务逻辑混合在一起, 因此为了单元测试而分离这些组件成为一项艰巨的任务。许多人忽略了这一任务, 什么都不测试 。

    Fuzzy Definition of “Manage” ("管理" 的模糊定义)

    视图控制器具有 "视图" 属性, 并且可以通过 Iboutlet 访问该视图的任何子视图。当您有多个 outlets 时, 就很难扩展, 此时最好使用 “子视图控制器” 来帮助管理所有 “子视图”。

    什么时候把事情分解得有好处?验证用户输入的业务逻辑是属于控制器还是模型?

    这里有多个模糊线, 似乎没有人能够完全达成一致。看来, 无论你在哪里画这些线, 视图和相应的控制器变得如此紧密耦合。

    MVVM

    根据MVC框架的分析,如何设计一个合理的架构模式来弥补MVC架构模式的缺陷呢?因此MVVM 应运而生。

    M-VC

    就是事而言,尽管 视图(View)和 视图控制器(ViewController)虽然是不同的组件, 但是他们是一一对应的,相互配对的(一个 view 只能与一个 controller 进行匹配),所以可以将 View 和 ViewController 看成一个整体。

    如何进一步解决VC 臃肿( Massive View Controller)的问题呢?

    MVVM 一种可以很好地解决 Massive View Controller 问题的办法就是将 Controller 中的展示逻辑(presentation logic:比如将值从模型转换为视图可以呈现的东西, 比如将NSDate并将其转换为格式化NSString.)抽取出来,放置到一个专门的地方,而这个地方就是 viewModel 。其实,我们只要在上图中的 M-VC 之间放入 VM ,就可以得到 MVVM 模式的结构图:

    MVVM

    视图模型 (ViewModel) 是放置用户输入验证逻辑、视图表示逻辑、网络请求启动和其他杂项代码的绝佳位置。视图模型 (ViewModel) 中绝对不可以对视图(View)本身进行引用。换句话说, 不要在视图模型中 #import uikit. h。

    MVVM架构和通信
    • view :由 MVC 中的 view 和 controller 组成,负责 UI 的展示,绑定 viewModel 中的属性,触发 viewModel 中的命令;

    • viewModel :从 MVC 的 controller 中抽取出来的展示逻辑,负责从 model 中获取 view 所需的数据,转换成 view 可以展示的数据,并暴露公开的属性和命令供 view 进行绑定;

    • model :与 MVC 中的 model 一致,包括数据模型、访问数据库的操作和网络请求等;

    • binder :在 MVVM 中,声明式的数据和命令绑定是一个隐含的约定,它可以让开发者非常方便地实现 view 和 viewModel 的同步,避免编写大量繁杂的样板化代码。在微软的 MVVM 实现中,使用的是一种被称为 XAML 的标记语言。在IOS中我们通常会使用KVO进行消息传递。

    MVVM 的优点

    • MVVM 是MVC 架构衍生与现有的 MVC 体系结构兼容。
    • MVVM 将显示逻辑从VC 中分离到VM中,有效解决VC 臃肿问题。
    • MVVM 将显示逻辑与业务逻辑进行剥离, 使得应用更易于单元测试。
    • MVVM 最适合使用绑定机制。

    MVVM 对象之间的通信方案

    相关文章

      网友评论

          本文标题:设计模式之 MVC 和 MVVM

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