iOS MVC 架构模式~详解

//联系人:石虎QQ: 1224614774昵称:嗡嘛呢叭咪哄

MVC

MVC的相关概念

MVC最早存在于桌面程序中的, M是指业务数据, V是指用户界面, C则是控制器. 在具体的业务场景中, C作为M和V之间的连接, 负责获取输入的业务数据, 然后将处理后的数据输出到界面上做相应展示, 另外, 在数据有所更新时, C还需要及时提交相应更新到界面展示. 在上述过程中, 因为M和V之间是完全隔离的, 所以在业务场景切换时, 通常只需要替换相应的C, 复用已有的M和V便可快速搭建新的业务场景. MVC因其复用性, 大大提高了开发效率, 现已被广泛应用在各端开发中.

随着需求的变更, UserVC变得越来越臃肿, 越来越难以维护, 拓展性和测试性也极差. 程序员也发现好像代码写得有些问题, 但是问题具体出在哪里? 难道这不是MVC吗?

iOS MVC 架构模式~详解_第1张图片

通过这张图可以发现, 用户信息页面作为业务场景Scene需要展示多种数据M(Blog/Draft/UserInfo), 所以对应的有多个View(blogTableView/draftTableView/image…), 但是, 每个MV之间并没有一个连接层C, 本来应该分散到各个C层处理的逻辑全部被打包丢到了Scene这一个地方处理, 也就是M-C-V变成了MM…-Scene-…VV, C层就这样莫名其妙的消失了.

另外, 作为V的两个cell直接耦合了M(blog/draft), 这意味着这两个V的输入被绑死到了相应的M上, 复用无从谈起.

最后, 针对这个业务场景的测试异常麻烦, 因为业务初始化和销毁被绑定到了VC的生命周期上, 而相应的逻辑也关联到了和View的点击事件, 测试只能Command+R, 点点点…

正确的MVC使用姿势

也许是UIViewController的类名给新人带来了迷惑, 让人误以为VC就一定是MVC中的C层, 又或许是Button, Label之类的View太过简单完全不需要一个C层来配合, 总之, 我工作以来经历的项目中见过太多这样的”MVC”. 那么, 什么才是正确的MVC使用姿势呢?

仍以上面的业务场景举例, 正确的MVC应该是这个样子的:

iOS MVC 架构模式~详解_第2张图片

serVC作为业务场景, 需要展示三种数据, 对应的就有三个MVC, 这三个MVC负责各自模块的数据获取, 数据处理和数据展示, 而UserVC需要做的就是配置好这三个MVC, 并在合适的时机通知各自的C层进行数据获取, 各个C层拿到数据后进行相应处理, 处理完成后渲染到各自的View上, UserVC最后将已经渲染好的各个View进行布局即可, 具体到代码中如下:

@interfaceBlogTableViewHelper:NSObject

+(instancetype)helperWithTableView:(UITableView *)tableViewuserId:(NSUInteger)userId;

-(void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander;

-(void)setVCGenerator:(ViewControllerGenerator)VCGenerator;

@end

@interfaceBlogTableViewHelper()

@property(weak,nonatomic)UITableView *tableView;

@property(copy,nonatomic)ViewControllerGeneratorVCGenerator;

@property(assign,nonatomic)NSUIntegeruserId;

@property(strong,nonatomic)NSMutableArray *blogs;

@property(strong,nonatomic)UserAPIManager *apiManager;

@end

#define BlogCellReuseIdentifier @"BlogCell"

@implementationBlogTableViewHelper

+(instancetype)helperWithTableView:(UITableView *)tableViewuserId:(NSUInteger)userId{

return[[BlogTableViewHelperalloc]initWithTableView:tableViewuserId:userId];

}

-(instancetype)initWithTableView:(UITableView *)tableViewuserId:(NSUInteger)userId{

if(self=[superinit]){

self.userId=userId;

tableView.delegate=self;

tableView.dataSource=self;

self.apiManager=[UserAPIManagernew];

self.tableView=tableView;

__weak typeof(self)weakSelf=self;

[tableViewregisterClass:[BlogCellclass]forCellReuseIdentifier:BlogCellReuseIdentifier];

tableView.header=[MJRefreshAnimationHeaderheaderWithRefreshingBlock:^{//下拉刷新

[weakSelf.apiManagerefreshUserBlogsWithUserId:userIdcompletionHandler:^(NSError *error,idresult){

//...略

}];

}];

tableView.footer=[MJRefreshAnimationFooterheaderWithRefreshingBlock:^{//上拉加载

[weakSelf.apiManageloadMoreUserBlogsWithUserId:userIdcompletionHandler:^(NSError *error,idresult){

//...略

}];

}];

}

returnself;

}

#pragma mark - UITableViewDataSource && Delegate

//...略

-(NSInteger)tableView:(UITableView *)tableViewnumberOfRowsInSection:(NSInteger)section{

returnself.blogs.count;

}

-(UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath{

BlogCell *cell=[tableViewdequeueReusableCellWithIdentifier:BlogCellReuseIdentifier];

BlogCellHelper *cellHelper=self.blogs[indexPath.row];

if(!cell.didLikeHandler){

__weak typeof(cell)weakCell=cell;

[cellsetDidLikeHandler:^{

cellHelper.likeCount+=1;

weakCell.likeCountText=cellHelper.likeCountText;

}];

}

cell.authorText=cellHelper.authorText;

//...各种设置

returncell;

}

-(void)tableView:(UITableView *)tableViewdidSelectRowAtIndexPath:(NSIndexPath *)indexPath{

[self.navigationControllerpushViewController:self.VCGenerator(self.blogs[indexPath.row])animated:YES];

}

#pragma mark - Utils

-(void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander{

[[UserAPIManagernew]refreshUserBlogsWithUserId:self.userIdcompletionHandler:^(NSError *error,idresult){

if(error){

[selfshowErrorInView:self.tableViewinfo:error.domain];

}else{

for(Blog *bloginresult){

[self.blogsaddObject:[BlogCellHelperhelperWithBlog:blog]];

}

[self.tableViewreloadData];

}

completionHandler?completionHandler(error,result):nil;

}];

}

//...略

@end

@implementationBlogCell

//...略

-(void)onClickLikeButton:(UIButton *)sender{

[[UserAPIManagernew]likeBlogWithBlogId:self.blogIduserId:self.userIdcompletionHandler:^(NSError *error,idresult){

if(error){

//do error

}else{

//do success

self.didLikeHandler?self.didLikeHandler():nil;

}

}];

}

@end

@implementationBlogCellHelper

-(NSString *)likeCountText{

return[NSStringstringWithFormat:@"赞 %ld",self.blog.likeCount];

}

//...略

-(NSString *)authorText{

return[NSStringstringWithFormat:@"作者姓名: %@",self.blog.authorName];

}

@end

Blog模块由BlogTableViewHelper(C), BlogTableView(V), Blogs(C)构成, 这里有点特殊, blogs里面装的不是M, 而是Cell的C层CellHelper, 这是因为Blog的MVC其实又是由多个更小的MVC组成的. M和V没什么好说的, 主要说一下作为C的TableVIewHelper做了什么.

实际开发中, 各个模块的View可能是在Scene对应的Storyboard中新建并布局的, 此时就不用各个模块自己建立View了(比如这里的BlogTableViewHelper), 让Scene传到C层进行管理就行了, 当然, 如果你是纯代码的方式, 那View就需要相应模块自行建立了(比如下文的UserInfoViewController), 这个看自己的意愿, 无伤大雅.

BlogTableViewHelper对外提供获取数据和必要的构造方法接口, 内部根据自身情况进行相应的初始化.

当外部调用fetchData的接口后, Helper就会启动获取数据逻辑, 因为数据获取前后可能会涉及到一些页面展示(HUD之类的), 而具体的展示又是和Scene直接相关的(有的Scene展示的是HUD有的可能展示的又是一种样式或者根本不展示), 所以这部分会以CompletionHandler的形式交由Scene自己处理.

在Helper内部, 数据获取失败会展示相应的错误页面, 成功则建立更小的MVC部分并通知其展示数据(也就是通知CellHelper驱动Cell), 另外, TableView的上拉刷新和下拉加载逻辑也是隶属于Blog模块的, 所以也在Helper中处理.

在页面跳转的逻辑中, 点击跳转的页面是由Scene通过VCGeneratorBlock直接配置的, 所以也是解耦的(你也可以通过didSelectRowHandler之类的方式传递数据到Scene层, 由Scene做跳转, 是一样的).

最后, V(Cell)现在只暴露了Set方法供外部进行设置, 所以和M(Blog)之间也是隔离的, 复用没有问题.

这一系列过程都是自管理的, 将来如果Blog模块会在另一个SceneX展示, 那么SceneX只需要新建一个BlogTableViewHelper, 然后调用一下helper.fetchData即可.

DraftTableViewHelper和BlogTableViewHelper逻辑类似, 就不贴了, 简单贴一下UserInfo模块的逻辑:

@implementationUserInfoViewController

+(instancetype)instanceUserId:(NSUInteger)userId{

return[[UserInfoViewControlleralloc]initWithUserId:userId];

}

-(instancetype)initWithUserId:(NSUInteger)userId{

//    ...略

[selfaddUI];

//    ...略

}

#pragma mark - Action

-(void)onClickIconButton:(UIButton *)sender{

[self.navigationControllerpushViewController:self.VCGenerator(self.user)animated:YES];

}

#pragma mark - Utils

-(void)addUI{

//各种UI初始化 各种布局

self.userIconIV=[[UIImageViewalloc]initWithFrame:CGRectZero];

self.friendCountLabel=...

...

}

-(void)fetchData{

[[UserAPIManagernew]fetchUserInfoWithUserId:self.userIdcompletionHandler:^(NSError *error,idresult){

if(error){

[selfshowErrorInView:self.viewinfo:error.domain];

}else{

self.user=[UserobjectWithKeyValues:result];

self.userIconIV.image=[UIImageimageWithURL:[NSURLURLWithString:self.user.url]];//数据格式化

self.friendCountLabel.text=[NSStringstringWithFormat:@"赞 %ld",self.user.friendCount];//数据格式化

...

}

}];

}

@end

UserInfoViewController除了比两个TableViewHelper多个addUI的子控件布局方法, 其他逻辑大同小异, 也是自己管理的MVC, 也是只需要初始化即可在任何一个Scene中使用.

现在三个自管理模块已经建立完成, UserVC需要的只是根据自己的情况做相应的拼装布局即可, 就和搭积木一样

iOS MVC 架构模式~详解_第3张图片
iOS MVC 架构模式~详解_第4张图片
iOS MVC 架构模式~详解_第5张图片
iOS MVC 架构模式~详解_第6张图片

作为业务场景的的Scene(UserVC)做的事情很简单, 根据自身情况对三个模块进行配置(configuration), 布局(addUI), 然后通知各个模块启动(fetchData)就可以了, 因为每个模块的展示和交互是自管理的, 所以Scene只需要负责和自身业务强相关的部分即可. 另外, 针对自身访问的情况我们建立一个UserVC子类SelfVC, SelfVC做的也是类似的事情.

MVC到这就说的差不多了, 对比上面错误的MVC方式, 我们看看解决了哪些问题:

1.代码复用: 三个小模块的V(cell/userInfoView)对外只暴露Set方法, 对M甚至C都是隔离状态, 复用完全没有问题. 三个大模块的MVC也可以用于快速构建相似的业务场景(大模块的复用比小模块会差一些, 下文我会说明).

2.代码臃肿: 因为Scene大部分的逻辑和布局都转移到了相应的MVC中, 我们仅仅是拼装MVC的便构建了两个不同的业务场景, 每个业务场景都能正常的进行相应的数据展示, 也有相应的逻辑交互, 而完成这些东西, 加空格也就100行代码左右(当然, 这里我忽略了一下Scene的布局代码).

3.易拓展性: 无论产品未来想加回收站还是防御塔, 我需要的只是新建相应的MVC模块, 加到对应的Scene即可.

4.可维护性: 各个模块间职责分离, 哪里出错改哪里, 完全不影响其他模块. 另外, 各个模块的代码其实并不算多, 哪一天即使写代码的人离职了, 接手的人根据错误提示也能快速定位出错模块.

5.易测试性: 很遗憾, 业务的初始化依然绑定在Scene的生命周期中, 而有些逻辑也仍然需要UI的点击事件触发, 我们依然只能Command+R, 点点点…

MVC的缺点

可以看到, 即使是标准的MVC架构也并非完美, 仍然有部分问题难以解决, 那么MVC的缺点何在? 总结如下:

1.过度的注重隔离: 这个其实MV(x)系列都有这缺点, 为了实现V层的完全隔离, V对外只暴露Set方法, 一般情况下没什么问题, 但是当需要设置的属性很多时, 大量重复的Set方法写起来还是很累人的.

2.业务逻辑和业务展示强耦合: 可以看到, 有些业务逻辑(页面跳转/点赞/分享…)是直接散落在V层的, 这意味着我们在测试这些逻辑时, 必须首先生成对应的V, 然后才能进行测试. 显然, 这是不合理的. 因为业务逻辑最终改变的是数据M, 我们的关注点应该在M上, 而不是展示M的V.

谢谢!!!

你可能感兴趣的:(iOS MVC 架构模式~详解)