设计模式之 MVC 和 MVVM
MVC
MVCMVC
设计模式是“模型-视图-控制器”。该模式不仅定义了对象在应用程序中扮演的角色, 还定义了对象之间的通信方式。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
提供了 UINavigationController
和 UITabBarController
这两种容器 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
模式的结构图:
视图模型 (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 最适合使用绑定机制。