7.6 开发模式
MVC
iOS - MVC 架构模式 - QianChia - 博客园
-
介绍
- Model和View永远不能相互通信,只能通过Controller传递。
- Controller可以直接与Model对话,Model通过Notification和KVO机制与Controller间接通信。
- Controller可以直接与View对话,通过outlet,直接操作View,outlet 直接对应到View中的控件,View通过action向Controller报告事件的发生。Controller是View的直接数据源。Controller是View的代理,以同步 View与Controller。
-
M:模型(Model)
- 模型对象封装了应用程序的数据,并定义操控和处理该数据的逻辑和运算。例如,模型对象可能是表示商品数据 list。用户在视图层中所进行的创建或修改数据的操作,通过控制器对象传达出去,最终会创建或更新模型对象。模型对象更改时(例如通过网络连接接收到新数据),它通知控制器对象,控制器对象更新相应的视图对象。
-
V:视图(view)
视图对象是应用程序中用户可以看见的对象。视图对象知道如何将自己绘制出来,可能对用户的操作作出响应。视图对象的主要目的就是显示来自应用程序模型对象的数据,并使该数据可被编辑。尽管如此,在 MVC 应用程序中,视图对象通常与模型对象分离。
在iOS应用程序开发中,所有的控件、窗口等都继承自 UIView,对应 MVC 中的 V。UIView 及其子类主要负责 UI 的实现,而 UIView 所产生的事件都可以采用委托的方式,交给 UIViewController 实现。
-
C:控制器(controller)
在应用程序的一个或多个视图对象和一个或多个模型对象之间,控制器对象充当媒介。控制器对象因此是同步管道程序,通过它,视图对象了解模型对象的更改,反之亦然。控制器对象还可以为应用程序执行设置和协调任务,并管理其他对象的生命周期。
控制器对象解释在视图对象中进行的用户操作,并将新的或更改过的数据传达给模型对象。模型对象更改时,一个控制器对象会将新的模型数据传达给视图对象,以便视图对象可以显示它。
对于不同的 UIView,有相应的 UIViewController,对应 MVC 中的 C。例如在 iOS 上常用的 UITableView,它所对应的 Controller 就是UITableViewController。
MVC 自身的不足
-
MVC 是一个用来组织代码的权威范式,也是构建 iOS App 的标准模式。Apple 甚至是这么说的。在 MVC 下,所有的对象被归类为一个 model,一个 view,或一个 controller。Model 持有数据,View 显示与用户交互的界面,而 View Controller 调解 Model 和 View 之间的交互。然而,随着模块的迭代我们越来越发现 MVC 自身存在着很多不足。
-
1)MVC 在现实应用中的不足:
- 在 MVC 模式中 view 将用户交互通知给控制器。view 的控制器通过更新 Model 来反应状态的改变。Model(通常使用 Key-Value-Observation)通知控制器来更新他们负责的 view。大多数 iOS 应用程序的代码使用这种方式来组织。
-
2)愈发笨重的 Controller:
在传统的 app 中模型数据一般都很简单,不涉及到复杂的业务数据逻辑处理,客户端开发受限于它自身运行的的平台终端,这一点注定使移动端不像 PC 前端那样能够处理大量的复杂的业务场景。然而随着移动平台的各种深入,我们不得不考虑这个问题。传统的 Model 数据大多来源于网络数据,拿到网络数据后客户端要做的事情就是将数据直接按照顺序画在界面上。随着业务的越来越来的深入,我们依赖的 service 服务可能在大多时间无法第一时间满足客户端需要的数据需求,移动端愈发的要自行处理一部分逻辑计算操作。这个时间一惯的做法是在控制器中处理,最终导致了控制器成了垃圾箱,越来越不可维护。
控制器 Controller 是 app 的 “胶水代码”,协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的 loading、appearing、disappearing 等等,同时往往也会充满我们不愿暴露的 Model 的模型逻辑以及不愿暴露给视图的业务逻辑。这引出了第一个关于 MVC 的问题...
视图 view 通常是 UIKit 控件(component,这里根据习惯译为控件)或者编码定义的 UIKit 控件的集合。进入 .xib 或者 Storyboard 会发现一个 app、Button、Label 都是由这些可视化的和可交互的控件组成。View 不应该直接引用 Model,并且仅仅通过 IBAction 事件引用 controller。业务逻辑很明显不归入 view,视图本身没有任何业务。
厚重的 View Controller 由于大量的代码被放进 viewcontroller,导致他们变的相当臃肿。在 iOS 中有的 view controller 里绵延成千上万行代码的事并不是前所未见的。这些超重 app 的突出情况包括:厚重的 View Controller 很难维护(由于其庞大的规模);包含几十个属性,使他们的状态难以管理;遵循许多协议(protocol),导致协议的响应代码和 controller 的逻辑代码混淆在一起。
厚重的 view controller 很难测试,不管是手动测试或是使用单元测试,因为有太多可能的状态。将代码分解成更小的多个模块通常是件好事。
-
3)太过于轻量级的 Model:
- 早期的 Model 层,其实就是如果数据有几个属性,就定义几个属性,ARC 普及以后我们在 Model 层的实现文件中基本上看不到代码(无需再手动管理释放变量,Model 既没有复杂的业务处理,也没有对象的构造,基本上 .m 文件中的代码普遍是空的);同时与控制器的代码越来厚重形成强烈的反差,这一度让人不禁对现有的开发设计构思有所怀疑。
-
4)遗失的网络逻辑:
苹果使用的 MVC 的定义是这么说的:所有的对象都可以被归类为一个 Model,一个 view,或是一个控制器。就这些,那么把网络代码放哪里?和一个 API 通信的代码应该放在哪儿?
你可能试着把它放在 Model 对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的 Model 生命周期更长,事情将变的复杂。显然也不应该把网络代码放在 view 里,因此只剩下控制器了。这同样是个坏主意,因为这加剧了厚重控制器的问题。那么应该放在那里呢?显然 MVC 的 3 大组件根本没有适合放这些代码的地方。
-
5)较差的可测试性
MVC 的另一个大问题是,它不鼓励开发人员编写单元测试。由于控制器混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。大多数人选择忽略这个任务,那就是不做任何测试。
上文提到了控制器可以管理视图的层次结构;控制器有一个 “view” 属性,并且可以通过 IBOutlet 访问视图的任何子视图。当有很多 outlet 时这样做不易于扩展,在某种意义上,最好不要使用子视图控制器(child view controller)来帮助管理子视图。在这里有多个模糊的标准,似乎没有人能完全达成一致。貌似无论如何,view 和对应的 controller 都紧紧的耦合在一起,总之,还是会把它们当成一个组件来对待。Apple 提供的这个组件一度以来在某种程度误导了大多初学者,初学者将所有的视图全部拖到 xib 中,连接大量的 IBoutLet 输出口属性,都是一些列问题。
-
-
示例
- model
// BookModel.h @interface BookModel : NSObject // 根据需要使用的数据创建数 Modal 数据模型属性变量 @property(nonatomic, copy)NSString *title; @property(nonatomic, copy)NSString *detail; @property(nonatomic, copy)NSString *icon; @property(nonatomic, copy)NSString *price; + (instancetype)bookModelWithDict:(NSDictionary *)dict; @end // BookModel.m @implementation BookModel + (instancetype)bookModelWithDict:(NSDictionary *)dict { BookModel *book = [[self alloc] init]; [book setValuesForKeysWithDictionary:dict]; return book; } @end
- view
// BookCell.h @class BookModel; @interface BookCell : UITableViewCell // 创建 Cell 视图包含的内容,Cell 使用 xib 创建 @property (weak, nonatomic) IBOutlet UIImageView *iconView; @property (weak, nonatomic) IBOutlet UILabel *titleLabel; @property (weak, nonatomic) IBOutlet UILabel *detailLabel; @property (weak, nonatomic) IBOutlet UILabel *priceLabel; // 创建 Cell 视图赋值方法 @property (nonatomic, strong) BookModel *bookModel; @end // BookCell.m // 包含数据模型头文件 #import "BookModel.h" @implementation BookCell // 从 Model 数据模型中取出数据更新 View 的内容 - (void)setBookModel:(BookModel *)bookModel { _iconView.image = [UIImage imageNamed:bookModel.icon]; _titleLabel.text = bookModel.title; _detailLabel.text = bookModel.detail; _priceLabel.text = bookModel.price; } @end
- controller
// ViewController.m // Modal 模型处理 // 声明数据源 @property (nonatomic, strong) NSArray *myDataArray; // 加载模型数据 - (NSArray *)myDataArray { if (_myDataArray == nil) { NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"bookData" ofType:@"plist"]]; NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:array.count]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { // KVC BookModel *bookModel = [BookModel bookModelWithDict:obj]; // 使用 Modal 数据模型初始化数据源数组 [arrayM addObject:bookModel]; }]; _myDataArray = [arrayM copy]; } return _myDataArray; } // View 视图处理 UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 20, self.view.bounds.size.width, self.view.bounds.size.height - 20)]; myTableView.delegate = self; myTableView.dataSource = self; [myTableView registerNib:[UINib nibWithNibName:@"BookCell" bundle:nil] forCellReuseIdentifier:@"BookCell"]; [self.view addSubview:myTableView]; - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return [self.myDataArray count]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 80; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ BookCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BookCell" forIndexPath:indexPath]; // 从 Modal 数据模型中取出数据更新 View 的内容 cell.bookModel = self.myDataArray[indexPath.row]; return cell; }
MVVM
iOS 关于MVVM Without ReactiveCocoa设计模式的那些事 - 简书
-
介绍
- 一种可以很好地解决Massive View Controller问题的办法就是将 Controller 中的展示逻辑抽取出来,放置到一个专门的地方,而这个地方就是 viewModel 。MVVM衍生于MVC,是对 MVC 的一种演进,它促进了 UI 代码与业务逻辑的分离。它正式规范了视图和控制器紧耦合的性质,并引入新的组件。他们之间的结构关系如下
- 在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件
- view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)
- viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方
- 使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性
-
M:模型(Model)
- 和MVC中的model保持一致,完全取决于你的"偏好设置"。你可能会为model封装一些额外的操作数据的业务逻辑,虽然苹果是推崇你这么干的,但是笔者认为不妥,这样很可能会导致一个胖Model的产生,而且胖Model相对比较难移植,胖Model随着产品的迭代会更加的Fat,最终难以维护,一胖毁所有。我更倾向于把它当做一个容纳表现数据-模型(data-model)对象信息的结构体(瘦Model),并通过一个单独的管理类来维护/创建/管理模型的统一逻辑,又或者可以通过使用Category来扩充业务逻辑。MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel(PS:感觉是否有点道理)。
-
V:视图(view)
- 由 MVC 中的view和 controller 组成,负责 UI 的展示,绑定 viewModel中的属性,触发 viewModel 中的命令以及呈现由viewModel提供的数据。
-
VM:视图模型(viewModel)
- 千万不要把它与传统数据-模型结构中模型混为一谈。 它的职责之一就是作为一个表现视图显示自身所需数据的静态模型;但它也有收集, 解释和转换那些数据的责任。它是从 MVC 的 controller 中抽取出来的展示逻辑,负责从 model中获取 view 所需的数据,转换成 view可以展示的数据,并暴露公开的属性和命令供 view 进行绑定
-
优点
- 减轻了控制器的负担,可以在VM中写网络层的逻辑,用户输入验证逻辑,视图显示逻辑等
-
注意
- view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)
- View持有ViewModel的引用,反之没有
-
ViewModel持有Model的引用,反之没有
MVP
- M:模型(Model)
- V:视图(view)
- P:主持(Presenter)