Open ShannonChenCHN opened 6 years ago
说明:下面两张图展示的是文章中用到的业务场景,图一是查看别人个人信息时的页面展示,图二是查看自己的个人信息的页面展示
误区:ViewController 就一定是 C 层?
正确的MVC应该是这个样子的:
大体架构:
细节(以 Blog 模块为例)
使用效果
代码复用:三个小模块的V(cell/userInfoView)对外只暴露Set方法, 对M甚至C都是隔离状态, 复用完全没有问题。三个大模块的MVC也可以用于快速构建相似的业务场景,大模块的复用比小模块会差一些
代码不再臃肿:因为Scene大部分的逻辑和布局都转移到了相应的MVC中, 我们仅仅是拼装MVC的便构建了两个不同的业务场景, 每个业务场景都能正常的进行相应的数据展示, 也有相应的逻辑交互, 而完成这些东西, 加空格也就100行代码左右
易拓展性:无论产品未来想加回收站还是防御塔, 我需要的只是新建相应的MVC模块, 加到对应的Scene即可
可维护性:各个模块间职责分离, 哪里出错改哪里, 完全不影响其他模块. 另外, 各个模块的代码其实并不算多, 哪一天即使写代码的人离职了, 接手的人根据错误提示也能快速定位出错模块
易测试性:很遗憾, 业务的初始化依然绑定在Scene的生命周期中, 而有些逻辑也仍然需要UI的点击事件触发, 我们依然只能Command+R, 点点点...(注:这一点不太明白,为什么说测试性不好?😆)
过度的注重隔离: 这个其实MV(x)系列都有这缺点, 为了实现V层的完全隔离, V对外只暴露Set方法, 一般情况下没什么问题, 但是当需要设置的属性很多时, 大量重复的Set方法写起来还是很累人的
业务逻辑和业务展示强耦合: 可以看到, 有些业务逻辑(页面跳转/点赞/分享...)是直接散落在V层的, 这意味着我们在测试这些逻辑时, 必须首先生成对应的V, 然后才能进行测试. 显然, 这是不合理的. 因为业务逻辑最终改变的是数据M, 我们的关注点应该在M上, 而不是展示M的V(注:这里同样不太理解。。。)
用 MVP 来实现前面提到的业务场景时,架构应该是这样的:
大体结构
业务逻辑被转移到了 P 层,此时的 V 层只需要做两件事:
C 层做的事情就是布局和 P-V 之间的绑定
细节(以 blog 模块为例)
One more thing
@interface VoiceCell ()
@end
@implementation VoiceCell
- (void)setPresenter:(VoiceCellPresenter *)presenter {
_presenter = presenter;
if (!presenter.didUpdatePlayStateHandler) {
__weak typeof(self) weakSelf = self;
[presenter setDidUpdatePlayStateHandler:^(NSUInteger playState) {
switch (playState) {
case PlayStateBuffering: weakSelf.playButton... break;
case PlayStatePlaying: weakSelf.playButton... break;
case PlayStatePaused: weakSelf.playButton... break;
}
}];
}
}
typedef void(^PlayCompletion)(NSError *error, id result);
typedef NS_ENUM(NSInteger, PlayState) {
PlayStateBuffering,
PlayStatePlaying,
PlayStatePaused,
PlayStateEnded,
};
@interface VoiceCellPresenter : NSObject
@property (copy, nonatomic) void(^didUpdatePlayStateHandler)(NSUInteger);
- (NSURL *)playURL;
- (void)playWithCompletionHandler:(PlayCompletion)completion;
@end
@implementation VoiceCellPresenter
- (void)playWithCompletionHandler:(PlayCompletion)comletion {
....
// 播放完成后,调用 didUpdatePlayStateHandler
if (self.didUpdatePlayStateHandler) {
self.didUpdatePlayStateHandler(PlayStateEnded);
}
}
@end
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.presenter = self.presenter.allDatas[indexPath.row];
__weak typeof(self) weakSelf = self;
__weak typeof(cell) weakCell = cell;
__block PlayCompletion playCompletion = nil;
__block __weak PlayCompletion weakCompletion = playCompletion;
playCompletion = ^(NSError *error, id result) {
// 找到下一个 cell 的 presenter
if (weakSelf.presenter.allDatas.count > indexPath.row + 1) {
BlogCellPresenter *nextPresenter = weakSelf.presenter.allDatas[indexPath.row + 1];
[nextPresenter playWithCompletionHandler: weakCompletion];
}
};
[cell setDidPlayHandler:^{
[weakCell.presenter playWithCompletionHandler:playCompletion];
}];
return cell;
}
MVP 的主要思想是把业务展示和业务逻辑分离开来,Presenter 层专门负责业务逻辑,View 层只管展示,Controller 将 Presenter 层和 View 层绑定起来,进行数据和事件通信。
在 V 层和 P 层之间进行无耦合的通信一般有两种方式:delegate 或者 block,也就是说,如果想要在 P 层更新数据后,通知 V 层更新的话,是绕不过这两种方式的。这两种方式可以将 V 层和 P 层隔离开,以实现解耦。
这有什么问题吗?问题就在于为了让 V 跟着数据的状态改变一起变化,我们需要写很多“通信”代码,要么是定义 block 属性,要么是定义 delegate 方法,当 V 层的监听的数据越多,就要定义越多的属性或方法。
除了 delegate 和 block 这两种通信方式之外,还有一种低耦合的通信方式就是 KVO。通过 KVO 的使用,我们可以省去很多胶水代码。
MVVM 与 MVP 相比,基本思想还是差不多的——将业务逻辑从 C 层中分离出来,只是,MVVM 相比 MVP 来讲最主要的特点就在于使用了数据绑定。所以,我们可以说,MVVM 其实只是 MVP 的绑定进化体,除去数据绑定方式,其他的和 MVP 如出一辙,只是可能呈现方式是Command/Signal 而不是 CompletionHandler 之类的。(???)
当然实现数据绑定的方式有很多种,除了 Apple 官方提供的 KVO 之外,比较广为人知的就是 ReactiveCocoa(简称 RAC) 了。
MVC:
MVP:
MVVM:
误区:MVVM 是为了解决 Controller 的臃肿和 MVC 难以测试问题
实际开发中,到底选哪个?
这篇文章按照架构的演进顺序,通过用三种不同的设计模式(MVC、MVP、MVVM)来实现一个示例场景,分别讲解了三种不同的设计模式各自的优缺点。