UIViewController瘦身

by admin on 2019年9月12日

这篇文章里会涉及如下几个方面:

鉴于这一阵子的面试经常被问到MVC模式的弊端(大家都知道的,ViewController会写入很多业务代码导致很臃肿),在此记录一下怎么给ViewController瘦身。

https://casatwy.com/iosying-yong-jia-gou-tan-wang-luo-ceng-she-ji-fang-an.html
https://casatwy.com/iosying-yong-jia-gou-tan-viewceng-de-zu-zhi-he-diao-yong-fang-an.html

随着程序逻辑复杂度的提高,你是否也发现了App中一些ViewController的代码行数急剧增多,达到了2,3千行,甚至更多。这时如果想再添加一点功能或者修改现有逻辑变得让人无比头疼。如果你遇到了这类问题,那是时候停下来了,思考一下如何更好地组织代码,给VC瘦身。本文将会阐述如何结合MVC的思想帮你的VC瘦身同时提高复用和可扩展性。

做iOS也有些年头了,最近把项目核心模块的架构重新设计了一番,这里做一些记录。首先,我们要对基础的设计模式有一定的认知。这些基础的设计模式,便是MVCMVVMVIPER

  • 1、代码的组织结构,以及为何要这样写。
  • 2、那些场景适合使用子控制器,那些场景应该避免使用子控制器?
  • 3、分离UITableView的数据源和UITableViewDataSource协议。
  • 4、MVVM的重点是ViewModel,不是响应函数式。
  • 5、MVVM中,ReactiveCocoa或RXSwift实现数据绑定的带来的弊端。
  • 6、用策略模式替代if-elseswitch这样判断比较多,不利于代码阅读的分支结构。并在特定场景下,用策略模式解决模块调用问题。
  • 7、为什么要较少模块间跨层数据交流。

主要有以下几方面:1、代码的组织结构,以及为何要这样写。2、那些场景适合使用子控制器,那些场景应该避免使用子控制器?3、分离UITableView的数据源和UITableViewDataSource协议。4、MVVM的重点是ViewModel,不是响应函数式。5、MVVM中,ReactiveCocoa或RXSwift实现数据绑定的带来的弊端。6、用策略模式替代if-else或switch这样判断比较多,不利于代码阅读的分支结构。并在特定场景下,用策略模式解决模块调用问题。7、为什么要较少模块间跨层数据交流。

一、开发中常见的现象和缺点

关于 MVC ,斯坦福的 Paul 老头有一张经典的图示,相信大部分iOSer都看过:

在说控制器瘦身之前,首先要做的的是保证代码结构的清晰化。良好的代码结构有利于代码的传承、可读性以及可维护性。通常笔者都是这样控制代码结构的:

一、代码结构

iOS中最常见的一种设计模式就是MVC,但在实际开发过程中,我们因为这样、那样的原因让单纯的ViewController变成了集Model,Controller以及View的一个大集合,这样势必就会导致VC的代码容量呈几何增长。这样的代码会存在以下几个问题:

图片 1mvc.png

#pragra mark - life cycle #pragra mark - notification #pragra mark - action #pragra mark - UITableViewDelegate.....总之这里是各种代理就对了#pragra mark - UI#pragra mark - setter & getter

在说控制器瘦身之前,首先要做的的是保证代码结构的清晰化。良好的代码结构有利于代码的传承、可读性以及可维护性。通常笔者都是这样控制代码结构的:

1、不利于后续维护

当有多个模块时,我们需要有多个 MVC 互相配合:

  • 1、不要在除了getter之外的结构中设置view基本坐标、属性等。
  • 2、在viewDidAppear里面做Notification的监听之类的事情。
  • 3、每一个代理方法都对应上相应的协议,否则后期随着代码量的增加,很难找出某一代理方法对应的协议,不利于代码的可读性。
  • 4、gettersetter 方法写在代码的最后面。
  • 5、getter方法中不要添加比较重要重要的业务逻辑,重要的业务逻辑应该单独拿出来,放在对应的pragra mark
    下,否则对于代码的阅读者来说,比较难以定位逻辑的入口位置。实际开发中遇到过多次这样的情况,焦头烂额的寻找关键逻辑入口处,纵里寻她千百度,结果它却躺在
    getter方法中。
  • 6、UI布局可以说比较重要,也可以说不重要。重要是因为一个新手接手新项目,如果对布局还没有了解,业务逻辑便无从谈起;UI布局不重要是因为只要相关控件封装的足够好,页面UI布局通常会很简单;因为UI布局比较重要,所以笔者将它放在固定位置(setter&getter上面),因为UI布局通常比较简单,所以将其放在代码中比较靠后的位置。
#pragra mark - life cycle #pragra mark - notification #pragra mark - action #pragra mark - UITableViewDelegate.....总之这里是各种代理就对了#pragra mark - UI#pragra mark - setter & getter

代码在一个公司的存活时间通常远长于你在公司的时间,你是否也在接手现有项目以后边看代码边在心里默念“a
piece of
shit”,我想没有一个人希望之后接手你代码的人有一天看你代码的时候也在心里默念着同样的话。作为一个有追求的程序员,或者说为了不被以后的同事骂,我们必须要为自己的代码负责,避免动辄就是几千行的一个源文件。你自己写的都不愿因看,更何况后续接手的人呢。

图片 2MVCs
working together.png

对于相对比较复杂的界面,通常情况下还可以考虑添加子控制的实现方式。如实际开发中,在商品搜索模块中,将历史搜索标签和推荐搜索标签、搜索推荐词条以及搜索结果用三个控制器分别承载不同的逻辑,是不同的代码逻辑分离。

1、不要在除了getter之外的结构中设置view基本坐标、属性等。2、在viewDidAppear里面做Notification的监听之类的事情。3、每一个代理方法都对应上相应的协议,否则后期随着代码量的增加,很难找出某一代理方法对应的协议,不利于代码的可读性。4、getter
和setter
方法写在代码的最后面。5、getter方法中不要添加比较重要重要的业务逻辑,重要的业务逻辑应该单独拿出来,放在对应的pragra
mark
下,否则对于代码的阅读者来说,比较难以定位逻辑的入口位置。实际开发中遇到过多次这样的情况,焦头烂额的寻找关键逻辑入口处,纵里寻她千百度,结果它却躺在
getter方法中。6、UI布局可以说比较重要,也可以说不重要。重要是因为一个新手接手新项目,如果对布局还没有了解,业务逻辑便无从谈起;UI布局不重要是因为只要相关控件封装的足够好,页面UI布局通常会很简单;因为UI布局比较重要,所以笔者将它放在固定位置(setter&getter上面),因为UI布局通常比较简单,所以将其放在代码中比较靠后的位置。

如果项目进度很赶,当时没有时间对代码进行合理的拆分和重构,后续也一定要抽出时间来填一下自己挖的坑。你可能会说,公司一直都很忙没有时间留给你去重构。我只能说要不就是你自己不会安排时间,要不就是这个公司只把你当代码搬运工。站在长远发展的角度上,要么改变自己,要么炒了老板。

可以看到,多个模块之间的交互都是通过 Controller 层。以上就是 MVC
的概览,那么 MVVM 是什么样的呢?

优点:把和该元素相关的业务逻辑分解一部分到子控制器中,主业务逻辑对应的代码量瞬间减少很多,代码封装和分离十分清晰。

二、 关于子控制器

2、不利于支撑UI的变动

MVVM 是 Model-View-ViewModel 的缩写。其实在 MVC 的基础上再稍进一步,把
Controller 与 View 之间的数据传递过程独立出来,封装成一个模块,叫做
ViewModel ,这就成了 MVVM 了。在 MVVM
的基础上,通常还会使用双向绑定技术,使得 View 和 ViewModel
之间可以自动同步。

缺点:这种做法最大的缺点就是父控制器和子控制器之间的消息传递有时需要做额外的处理,尤其是子控制器的消息回调。

对于相对比较复杂的界面,通常情况下还可以考虑添加子控制的实现方式。优点:把和该元素相关的业务逻辑分解一部分到子控制器中,主业务逻辑对应的代码量瞬间减少很多,代码封装和分离十分清晰。缺点:这种做法最大的缺点就是父控制器和子控制器之间的消息传递有时需要做额外的处理,尤其是子控制器的消息回调。所以,建议根据实际情况有选择的考虑,如果父控制器和子控制器之间的消息交互较少,完全可以考虑此种方式。如果父控制器和子控制器之间的消息交互较多,建议仔细考虑清楚再做取舍。

试想如果有一天你们的App的UI风格需要改变,大量的View需要改变,在一个几千行的VC中删删改改是什么感受。可能因为改动UI的时候一个不留神错改了Model或者Controller的东西,导致程序都不能正常运行。而且改动的范围不能控制在一个较小的范围,测试回归起来的工作量也是很大的。

图片 3viper.png

所以,建议根据实际情况有选择的考虑,如果父控制器和子控制器之间的消息交互较少,完全可以考虑此种方式。如果父控制器和子控制器之间的消息交互较多,建议仔细考虑清楚再做取舍。

实际开发中,苹果专门提供了一个UITableViewController类,专门为tableView服务,但是实际开发中很少有人直接使用。该控制相对于普通的UIViewController的而言,直接实现了下拉刷新功能;除此之外,还能切换编辑模式、响应键盘通知。如果UITableViewController实现的标准刚好同你项目中的tableView一些需求很类似,就可以直接通过使用子控制器的方式,避免了写那些重复的代码。当然,实际开发中出现这种事情的概率非常小。这里仅是简单提示下。

3、不利于复用

VIPER ,全称 View-Interactor-Presenter-Entity-Router 。这是另一种细分
MVC 而得到的架构。从上图可以看到, VIPER 实际上是将 MVC 中的 Controller
细化为了三个模块,即 Presenter、Interactor、Router 。 Entity
负责数据持久化, Interactor 负责业务相关的逻辑计算等, Presenter
则负责将业务数据传递给 View ,也负责处理 View 的事件。大部分 View
的事件是交由逻辑侧 interactor 处理,在 interactor 处理完后会触发必要的
UI 刷新。跳转相关的 View 事件则交由 Router 处理。

实际开发中,苹果专门提供了一个UITableViewController类,专门为tableView服务,但是实际开发中很少有人直接使用。该控制相对于普通的UIViewController的而言,直接实现了下拉刷新功能;除此之外,还能切换编辑模式、响应键盘通知。如果UITableViewController实现的标准刚好同你项目中的tableView一些需求很类似,就可以直接通过使用子控制器的方式,避免了写那些重复的代码。当然,实际开发中出现这种事情的概率非常小。这里仅是简单提示下。

三、UITableView 的瘦身

如果你的App一开始只支持iPhone版本,所有的一切都那么自然,程序也运行的很好。突然有一天老板告诉你说公司业务发展的不错,为了扩大市场需要退出iPad版,这个时候如果仅仅只是iPhone版本的放大版,那么你需要做的可能就是添加一些view的自适应就好了。但现实并不总是理想,如果iPad版的需要重新设计,按钮的位置都变动了(参考上面的第二点),这个时候难道需要把所有的代码都改一遍?这尼玛工作量也太巨大了吧。

可以看到, VIPER 和 MVVM 并不矛盾,我们可以在 MVVM 的基础上继续细化得到
VIPER , ViewModel 相关的逻辑放在 Presenter 中即可。

绝大多数情况下,只要有控制器就会存在UITableView或UICollectionView(这里仅仅以tableView为例),所以对UITableView
的瘦身尤为重要。以下的分析主要参照该文章。

绝大多数情况下,只要有控制器就会存在UITableView或UICollectionView(这里仅仅以tableView为例),所以对UITableView
的瘦身尤为重要。以下的分析主要参照该文章。

通常iPhone版本和iPad版本不管UI怎么变,业务逻辑只是基本相同的,那么如果当初我们的代码层级比较清晰,是不是Controller和Model层就可以完美的复用呢,针对不同的版本换一套View即可,这个是不是方便多了,自己感受一下。

同样,当有多个模块时,我们需要有多个 VIPER 互相配合。

毫无疑问,在Controller层中协调View和Model的工作是无法拆除的。那么除此之外,不是必须有Controller层承载的内容便可以被拆除,比如tableView的数据源和UITableViewDataSource协议。下面分两种情况说明,一种是将数据源和UITableViewDataSource协议都拆分出来,另一种是只拆分数据源。

3.1 拆分出不重要的东西

二、如何解决这些问题

可以看到传统架构的进化过程: MVC -> MVVM -> VIPER
。这是一个对架构不断细化的过程。在工程实践中,我们的业务采用什么架构,需要根据业务的形态和频繁变动的模块而定。

3.1.1 单一的cell和数据源(拆分数据源和UITableViewDataSource协议)

关于这种情况,文章中实现代码如下这样。

//控制器中代码TableViewCellConfigureBlock configureCell = ^(PhotoCell *cell, Photo *photo) { [cell configureForPhoto:photo]; }; NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos; self.photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos cellIdentifier:PhotoCellIdentifier configureCellBlock:configureCell]; self.tableView.dataSource = self.photosArrayDataSource; [self.tableView registerNib:[PhotoCell nib] forCellReuseIdentifier:PhotoCellIdentifier];

//抽离的数据源代码//.h文件typedef void (^TableViewCellConfigureBlock)(id cell, id item);@interface ArrayDataSource : NSObject <UITableViewDataSource>- initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;- itemAtIndexPath:(NSIndexPath *)indexPath;@end//.m文件#import "ArrayDataSource.h"@interface ArrayDataSource ()@property (nonatomic, strong) NSArray *items;@property (nonatomic, copy) NSString *cellIdentifier;@property (nonatomic, copy) TableViewCellConfigureBlock configureCellBlock;@end@implementation ArrayDataSource- init{ return nil;}- initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock{ self = [super init]; if  { self.items = anItems; self.cellIdentifier = aCellIdentifier; self.configureCellBlock = [aConfigureCellBlock copy]; } return self;}- itemAtIndexPath:(NSIndexPath *)indexPath{ return self.items[(NSUInteger) indexPath.row];}#pragma mark UITableViewDataSource- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return self.items.count;}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier forIndexPath:indexPath]; id item = [self itemAtIndexPath:indexPath]; self.configureCellBlock(cell, item); return cell;}@end

上述代码将tableView的数据源以及UITableViewDataSource协议都抽离到ArrayDataSource中,而UITableViewDelegate依然保留在Controller层中。至于数据源的来源(实际中往往是从网络获取),这里主要是通过Store类获取数据,具体体现代码NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;,该行代码你可以理解为实际项目中的网络请求的伪代码。

毫无疑问,在Controller层中协调View和Model的工作是无法拆除的。那么除此之外,不是必须有Controller层承载的内容便可以被拆除,比如tableView的数据源和UITableViewDataSource协议。下面分两种情况说明,一种是将数据源和UITableViewDataSource协议都拆分出来,另一种是只拆分数据源。

第一部分说了这么多终于点题了,如何使用MVC模式更好的给VC瘦身,提高复用性和可维护性呢?我画了下面一个图:

不知大家有没有发现,以上所述的架构解决的是单个业务模块内的职责划分问题,并没有解决如何将多个业务模块组合在一起的问题。即多个
MVC 或者 多个 VIPER 之间如何配合?实践中我们发现:

3.1.2 多种数据源和多种cell(仅拆分数据源和Protocols)

同时拆分数据源和UITableViewDataSource协议这种方式有一定的局限性,如果一个tableView中有多类型cell,下面的这个方法将很难设计,尤其是针对参数aCellIdentifieaConfigureCellBlock。所以针对这种情况仅仅将tableView
的dataSource拆出来即可,实际这种拆分情况就是MVVM模式中的ViewModel。

- initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;

3.1.1 单一的cell和数据源(拆分数据源和UITableViewDataSource协议)

图片 4

  • 通过对 MVC 的进一步细分,可以从单个业务模块的角度上缓解 MVC 中
    Controller 中心化所导致的 massive view controller
    的问题,但对于有众多业务模块的 Controller 来说, massive view
    controller 依然得不到解决,即中心化的 Controller
    需要做大量胶水层的工作,管理各个子 Controller 。
  • 用好传统架构,可以保证单个业务模块内的代码的可复用性,但并不能避免业务之间的互相影响。简单说,就是修改业务
    A 的 bug 时,可能会给业务 B 引入 bug 。
3.1.3 网络层放在那里?

按照该文章的意思,网络层可以在封装之后放在Cotroller中,这种方案当然是可以。除此之外,按照MVVM的设计模式,网络层同样可以放在ArrayDataSource类中,该类对外提供网络请求接口,数据返回后,同步更新内部的数据源即可。

通常控制cell的状态我们可以实现如下两个代理方法。

- tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath{ PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor]; cell.photoTitleLabel.shadowOffset = CGSizeMake;}- tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath{ PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.photoTitleLabel.shadowColor = nil;}

按照这种方式会纯在两个缺点:

  • 1、这两个代理方法增加了Controller的代码量。
  • 2、在Controller中显示通过tableView获取cell,再调用cell实现的细节方法。思路相对比较绕。综上所述,文章中是在cell中控制cell的状态。代码如下。

- setHighlighted:highlighted animated:animated{ [super setHighlighted:highlighted animated:animated]; if (highlighted) { self.photoTitleLabel.shadowColor = [UIColor darkGrayColor]; self.photoTitleLabel.shadowOffset = CGSizeMake; } else { self.photoTitleLabel.shadowColor = nil; }}

另外一种好的做法就是将cell的根据model更新的方法,拆分到分类中完成。实际开发中,可能存在复杂的cell代码量很大,此时可以借助分类的方法分离关注点。

  • 1、MVVM本质上也是从MVC中派生出来的思想,由M、V、VM、V四部分组成,主要是为了减少MVC中Controller承担的负荷。
  • 2、借助ViewModel可以降低View和Model的耦合度。
  • 3、虽然ViewModel是MVVM组成的一部分,但是MVC中依然能用,上述分离tableView的dataSource就是很好的说明。MVVM的关键是要有ViewModel。而不是ReactiveCocoa、RXSwift或RXJava等。

  • 4、ReactiveCocoa或RXSwift只是能更好的体现能更好地体现MVVM的精髓。使用函数响应式框架能更好的实现数据和视图的双向绑定(ViewModel的数据可以显示到View上,View上的操作同样会引起ViewModel的变化),降低了ViewModel和View的耦合度。
  • 5、ReactiveCocoa或RXSwift不应该因为他本身难以被理解而被神化。通过这两个框架可以实现ViewModel和View的双向绑定,但同样会存在几个比较重大的问题。
    首先,ReactiveCocoa或RXSwift的学习成本很高;其次,数据绑定使得 Bug
    很难被调试,当界面出现异常,可能是View的问题,也可能是数据ViewModel的问题。而数据绑定会使一个位置的bug传递到其他位置,难以定位;最后,数据绑定是需要消耗更多的内存,对于大型项目更是如此。只是结合自己所学知识谈谈理解,如果对RXSwift感兴趣,推荐这个链接。

如果模块被拆分的太粗糙,基本就是简单的封装,并没有进一步细化,只是将所有的功能集中在一起,这样做似乎没有太大意义

如果模块被拆分的很细,Controller中很执行相关模块的功能就要调用相关模块代码,似乎代码量并不会减少太多。比如在做即时通信应用开发时,支持的消息类型有文字、语音、图片、视频消息。其中后三种消息类型同文字消息不同,后三者要求发送消息的时候,首先要像后台请求上传资源的权限,获取上传资源权限后,返回对应的字段(该字段以实际情况不同,可能是id,也可能是token之类的),上传成功后获取资源对应的URL,再把资源的URL通过类似文字消息的发送方式发送出去。此时,可以拆分成三个模块数据发送、上传资源申请、内容上传。如果要发送文字消息,直接在Controller中调用模块A即可;但是如果想发送其他消息,就要依次调用模块B、模块C、模块A,按照这种调用方式,Controller必然会膨胀。

在说合理拆分模块之前,先简单说下策略模式,因为接下来举的例子中涉及策略模式。策略模式一般是指:

1. 可以实现目标的方案集合;2. 根据形势发展而制定的行动方针和斗争方法;3. 有斗争艺术,能注意方式方法。

switch,if-else之类的分支语句,此类语句给人的直观感觉是判断条件明确,代码层次清晰,缺点可能是代码繁琐,杂乱无章,而且拆分困难。特别是到后期维护代码的时候,这种状况往往令人有食之无味,弃之可惜的感觉。使用策略模式可以代替switch或if-else之类的代码。举个例子,以下是小明的计划安排:

 周一打篮球 周二逛街 周三洗衣服 周四打游戏 周五唱歌 其他休息

借助策略模式我们可以这样实现代码:

@interface XiaoMing : NSObject- doSomethingWithDayStr:(NSString *)dayStr params:(NSDictionary *)paramsDict;@end

#import "XiaoMing.h"@interface XiaoMing()@property(nonatomic,copy)NSDictionary *strategyDict;//策略@property(nonatomic,copy)NSDictionary *paramDict;//参数@end@implementation XiaoMing- doSomethingWithDayStr:(NSString *)dayStr params:(NSDictionary *)paramsDict{ self.paramDict = paramsDict; if (self.strategyDict[dayStr]){ NSInvocation *doWhat = self.strategyDict[dayStr]; [doWhat invoke]; }else{ [self sleep]; }}- (NSInvocation *)invocationWithMethod:selector{ NSMethodSignature*signature = [[self class] instanceMethodSignatureForSelector:selector]; if (signature == nil) { NSString *reason = [NSString stringWithFormat:@"提示:The method[%@] is not find", NSStringFromSelector]; @throw [NSException exceptionWithName:@"错误" reason:reason userInfo:nil]; } NSInvocation*invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = self; invocation.selector = selector; NSDictionary *param = self.paramDict; //index表示第几个参数,注意0和1已经被占用了(self和_cmd),所以我们传递参数的时候要从2开始。 [invocation setArgument:& atIndex:2]; return invocation;}- playBasketball:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- shopping:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- washClothes:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- playGames:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- sing:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- sleep{ NSLog(@"这是其他情况:%s",__FUNCTION__);}- (NSDictionary *)strategyDict{ if (_strategyDict == nil) { _strategyDict = @{ @"day1" : [self invocationWithMethod:@selector(playBasketball:)], @"day2" : [self invocationWithMethod:@selector(shopping:)], @"day3" : [self invocationWithMethod:@selector(washClothes:)], @"day4" : [self invocationWithMethod:@selector(playGames:)], @"day5" : [self invocationWithMethod:@selector] }; } return _strategyDict;}@end

外部调用可以完全不再使用if-else的判断了。

XiaoMing *xm = [[XiaoMing alloc]init]; //各种情况直接赋值给dayStr即可。 NSString *dayStr = @"day3s"; [xm doSomethingWithDayStr:dayStr params:@{@"key":@"test"}];

关于上述问题我们可以通过组合和策略模式解决。首先创建一个MessageManager类。对外提供的接口大概是这样的:

typedef NS_ENUM (NSUInteger, MessageSendStrategy){ MessageSendStrategyText = 0, MessageSendStrategyImage = 1, MessageSendStrategyVoice = 2, MessageSendStrategyVideo = 3}@protocol MessageManagerDelegate<NSObject> @required - messageSender:(MessageSender *)messageSender didSuccessSendMessage:(BaseMessage *)message strategy:(MessageSendStrategy)strategy; - messageSender:(MessageSender *)messageSender didFailSendMessage:(BaseMessage *)message strategy:(MessageSendStrategy)strategy error:(NSError *)error;@end@interface MessageManager:NSObject@property (nonatomic, weak) id<MessageSenderDelegate> delegate;@property(nonatomic,copy)NSDictionary *strategyDict;//主要在这里定义策略,内部通过Invoke唤起对应方法。- sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy;@end

外部调用形式大概是这样的,除此之外还要遵守MessageManagerDelegate协议并实现协议方法。

[self.messageManager sendMessage:message withStrategy:MessageSendStrategyText];

MessageManager.m文件实现大概是这样的:

@interface MessageManager()@end@implementation MessageManager- sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy{ ..... if (self.strategyDict[@]){ NSInvocation *doWhat = self.strategyDict[@]; [doWhat invoke]; } ...... ......}............@end

总的来说,基本形式和上面举出的例子类似。

前面注意事项。那么接下来就是要使用模块了,使用模块的时候同样有需要注意的地方:减少跨层数据交流。举个例子,假如有模块一、模块二、模块三,按照正常的调用方式是外部使用模块一调用模块二的方法,模块二的方法再调用模块三的方法。但是随着模块功能的完善,突然有一天出现模块一直接调用模块三的情况,那么后续就很难避免其他开发人员可能直接拿模块一调用模块三方法。类似这种跨层的数据交流很不利于项目的后续维护。

除此之外,控件的合理拼装也能在很大程度上减少控制器中的代码。另外还有一个要注意的地方,就是UIViewController继承的问题。关于这个问题,可以在笔者之前写的这篇文章的第二部分内容找到答案。

在讲 UIResponder
之前可以先温习下响应链。脑海中想象一下这样的视图层级结构:一个
UIViewController 上放置一个 tableView(supTableView) , supTableView 的
cell 上放置一个 tableView(subTableView) , subTableView 的 cell 上有一个
UIButton 。即:

UIViewController -> SuperTable -> SuperCell -> SubTable -> SubCell -> UIButton

如果想在点击UIButton的时候在UIViewController中产生回调,一般可以借助delegate或block实现,但是由于层级太深,这样做的话会很繁琐。明智的方式是借助UIResponder。

只需要一个 UIResponder 的 category 就行

@interface UIResponder - routerEventWithSelectorName:(NSString *)selectorName object:object userInfo:(NSDictionary *)userInfo;@end

@implementation UIResponder - routerEventWithSelectorName:(NSString *)selectorName object:object userInfo:(NSDictionary *)userInfo { [[self nextResponder] routerEventWithSelectorName:selectorName object:object userInfo:userInfo];}@end

UIButton点击事件

- btnClick:(UIButton *)sender { [self routerEventWithSelectorName:@"btnClick:userInfo:" object:sender userInfo:@{@"key":@"value"}];}

在 Cell 内部获取父控制器,在 Cell
内部调用控制器的一些耦合性比较小的代码,一定程度上也能达到瘦身的目的。如在
Cell 中有个返回按钮,需要当前父视图控制器返回 Push
到它之前的控制器,那么就需要在自定义 Cell 中拿到当前的父视图控制器做 Pop
操作。

- (UIViewController *)viewController { for (UIView* next = [self superview]; next; next = next.superview) { UIResponder *nextResponder = [next nextResponder]; if ([nextResponder isKindOfClass:[UIViewController class]]) { return (UIViewController *)nextResponder; } } return nil;}

在说UIViewController的瘦身计划之前,第一部分先说了合理的代码结构;第二部分单提了下关于子控制器,并简单的用UITableViewController举了个例子;第三部分重点介绍了UITableView的瘦身,并因此引申出了MVVM的一些内容;第四部分主要介绍了一些模块拆分中遇到的一个问题和解决方案,除此还说明了模块跨层数据交流的问题;最后,提了下控件的拼装和UIViewController继承的问题。

关于这种情况,文章中实现代码如下这样。

解释一下上面这幅图,一个完整的模块被分为了三个相对独立的部分,分别是Model,View,Controller,对应到我们App中的依次为继承自NSObject的数据中心,承载UI展示和事件响应的View以及我们最最常用的UIViewController。

归根结底,就是因为没有一种更为宏观的组合模块的架构体系。正是为了解决如何将多个业务模块组合在一起的问题,我设计了一套
BC 的架构体系。

//控制器中代码TableViewCellConfigureBlock configureCell = ^(PhotoCell *cell, Photo *photo) { [cell configureForPhoto:photo]; }; NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos; self.photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos cellIdentifier:PhotoCellIdentifier configureCellBlock:configureCell]; self.tableView.dataSource = self.photosArrayDataSource; [self.tableView registerNib:[PhotoCell nib] forCellReuseIdentifier:PhotoCellIdentifier];

//抽离的数据源代码//.h文件typedef void (^TableViewCellConfigureBlock)(id cell, id item);@interface ArrayDataSource : NSObject <UITableViewDataSource>- initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;- itemAtIndexPath:(NSIndexPath *)indexPath;@end//.m文件#import "ArrayDataSource.h"@interface ArrayDataSource ()@property (nonatomic, strong) NSArray *items;@property (nonatomic, copy) NSString *cellIdentifier;@property (nonatomic, copy) TableViewCellConfigureBlock configureCellBlock;@end@implementation ArrayDataSource- init{ return nil;}- initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock{ self = [super init]; if  { self.items = anItems; self.cellIdentifier = aCellIdentifier; self.configureCellBlock = [aConfigureCellBlock copy]; } return self;}- itemAtIndexPath:(NSIndexPath *)indexPath{ return self.items[(NSUInteger) indexPath.row];}#pragma mark UITableViewDataSource- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return self.items.count;}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier forIndexPath:indexPath]; id item = [self itemAtIndexPath:indexPath]; self.configureCellBlock(cell, item); return cell;}@end

其中VC持有View和Model部分,View通过代理或者Target-Action的方式把用户的操作传递给VC,VC负责根据不同的用户行为做出不同响应。如果需要加载或刷新数据则直接调用Model暴露的接口,如果数据可以同步拿到,则直接使用获取到的数据刷新View。如果数据需要通过网络请求等其他异步的方式获取,VC则通过监听Model发出的数据更新(成功或失败)通知,在收到通知时根据成功或者失败对View进行相应的刷新操作。可以看出来整个过程中View和Model是没有直接交互的,所有的操作都是通过VC进行协调的。

BC ,全称 BusinessController
,是一种为解决业务模块耦合和管理问题而生的架构体系。

上述代码将tableView的数据源以及UITableViewDataSource协议都抽离到ArrayDataSource中,而UITableViewDelegate依然保留在Controller层中。至于数据源的来源(实际中往往是从网络获取),这里主要是通过Store类获取数据,具体体现代码NSArray
*photos = [AppDelegate
sharedDelegate].store.sortedPhotos;,该行代码你可以理解为实际项目中的网络请求的伪代码。

进过MVC分层的好处:

为了表明 BC 的思想和实践效果,这里我以 UIViewController
的瘦身为例进行阐述。众所周知, iOS 开发最让人头痛的问题之一就是
UIViewController 的代码过于庞大,难以维护。更有网友戏谑称 MVC 为 massive
view controller 。

3.1.2 多种数据源和多种cell(仅拆分数据源和Protocols)

1、VC代码量骤降,易于维护

iOS 系统默认以 UIViewController 扮演 Controller 的角色,推出一个界面就是
push 一个 UIViewController 。因此作为一个界面的总管, UIViewController
管理着各个子模块,也包揽了众多的边界模糊的工作。每当我们需要新增一个业务功能,首先就要找到对应的
UIViewController ,再在其中进行编码,如下述代码所示:

同时拆分数据源和UITableViewDataSource协议这种方式有一定的局限性,如果一个tableView中有多类型cell,下面的这个方法将很难设计,尤其是针对参数aCellIdentifie和aConfigureCellBlock。所以针对这种情况仅仅将tableView
的dataSource拆出来即可,实际这种拆分情况就是MVVM模式中的ViewModel。

可以看到拆分后VC中就仅剩下事件的响应操作了,所有显示相关的东西都被单独抽取出来,所有的网络请求以及数据缓存都被提取到出去了。VC中的代码会大幅度减少,在我们项目中的实践结果为:拆分前一个VC的代码行数为2600行,拆分后VC的代码行数仅剩不到600行。

@interface ViewController ()@property (nonatomic, assign) BOOL A_LogicFlag;@property (nonatomic, assign) BOOL B_LogicFlag;... (keep adding flags)@property (nonatomic, strong) A_ControllerClass *A_Controller;@property (nonatomic, strong) B_ControllerClass *B_Controller;... (keep adding modules)@end@implementation ViewController- viewDidLoad { [super viewDidLoad]; self.A_Controller = [A_ControllerClass new]; [self.view addSubview:self.A_Controller.view]; __weak typeof weakSelf = self; [self.A_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.A_LogicFlag = YES; }]; self.B_Controller = [B_ControllerClass new]; self.B_Controller.delegate = self.A_Controller; ... (keep adding code)}- viewWillAppear:animated { [super viewWillAppear:animated]; __weak typeof weakSelf = self; [self.B_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.B_LogicFlag = YES; }]; ... (keep adding code)}@end
- initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;

2、复用性提高

以上代码已经把每一个业务逻辑封装为一个个模块,然后在 UIViewController
中管理和维系各个业务模块间的关系,这是我们日常工作中最常见的代码。很明显,随着业务模块的不断增加,整个
UIViewController 的代码量将会无上限的增加。并且各个业务都在这个
UIViewController 中修改代码,很容易互相引入bug,产生耦合。

3.1.3 网络层放在那里?

拆分后如果App需要对UI展示进行大改,那么你的改动基本上都会停留在View模块中,你可以选择在现有的基础上改,也可以选择从写一个。只要业务不变的话,Model和VC模块完全不需要修改。这样改动的范围较小,对开发和测试都比较友好。

如果有细心的读者,会发现这其中还有时序问题。怎么讲?假设现在我们有一个模块
C ,我们想要做一个小改动:将 A 模块的初始化时机放在 C
模块的数据请求返回成功后。这是个很简单的改动,只需将 A
模块的初始化工作放入 C 模块的数据请求返回的 completion block 里:

按照该文章的意思,网络层可以在封装之后放在Cotroller中,这种方案当然是可以。除此之外,按照MVVM的设计模式,网络层同样可以放在ArrayDataSource类中,该类对外提供网络请求接口,数据返回后,同步更新内部的数据源即可。

拆分后如果App需要支持iPad版本,那么你需要做的就只是重写一个View然后放进去就好了,Model和VC模块同样基本上不要做任何修改,想想是不是还有点儿小激动呢。

- viewDidLoad { [super viewDidLoad]; self.C_Controller = [C_ControllerClass new]; __weak typeof weakSelf = self; [self.C_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.A_Controller = [A_ControllerClass new]; [weakSelf.view addSubview:weakSelf.A_Controller.view]; [weakSelf.A_Controller sendRequestOnCompletion:^(BOOL success){ weakSelf.A_LogicFlag = YES; }]; }]; self.B_Controller = [B_ControllerClass new]; self.B_Controller.delegate = self.A_Controller; ... (keep adding code)}

3.2 cell内部控制cell状态

三、总结

若不仔细看看,难以发现以上代码已经有了 bug 。因为我们延迟了
A_Controller 的初始化,所以在 B_Controller 设置 delegate 时,写入的
A_Controller 是 nil 。这就是时序依赖, B_Controller 在设置 delegate
时,要求 A_Controller
已经完成了初始化。看似这种时序问题在所难免,其实不然。在 BC
架构中,我将描述一种解决该时序问题的方案。

通常控制cell的状态我们可以实现如下两个代理方法。

使用MVC模式可以达到帮VC瘦身,可以到到提高复用性和可维护性的效果,同时也会让原本一个整体的业务代码,分散到各个不同的地方。实际使用中我们需要具体问题具体分析,如果一个VC中的东西本身就很简单,那么完全可以放在一起,因为即使全部放在一起也就几百行代码。例如App中常见的Copyright界面,本身就是加载一个html就搞定了,就完全没必要搞得那么复杂;如果一个VC已经很复杂,代码动辄几千行了,那么就需要拆分,达到更好的复用以及方便维护的目的。

另外,由于 coder 在 VC 中有着极高的自由度,所以当 coder
在做一些小特性时,会直接把代码写在 VC
中。大家为省事不再去为小功能独立创建模块,这样 VC
中的代码会更加混乱不堪。

- tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath{ PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor]; cell.photoTitleLabel.shadowOffset = CGSizeMake;}- tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath{ PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.photoTitleLabel.shadowColor = nil;}

写了几年代码了,见过所有东西都往一个源文件里面塞的,也见过一个本就很简单的东西被设计模式搞的四分五裂的,不要为了用设计模式而用设计模式。把握好度很重要,能用子弹解决的问题就不要动大炮。

  • 无限增长的代码量
  • 鱼龙混杂的耦合关系
  • 复杂的时序问题
  • 过度自由引入的混乱

按照这种方式会纯在两个缺点:

代码重构应该是一个持久的过程,在开发的过程中就时不时的对现有不合理的地方进行重构,而不是等待问题已经很多了才想起重构来。千里之行始于足下,千里之堤溃于蚁穴。

让我们来看看 BC 的架构体系如何来解决这些问题。

1、这两个代理方法增加了Controller的代码量。2、在Controller中显示通过tableView获取cell,再调用cell实现的细节方法。思路相对比较绕。综上所述,文章中是在cell中控制cell的状态。代码如下。

我们让 UIViewController 只负责持有和维护一个业务模块(
businessController
)的数组,其并不关心数组中每个业务模块的具体实现。我们定义一个
businessController 的基类,或者协议。这里我们以协议为例,定义协议
BusinessController

- setHighlighted:highlighted animated:animated{ [super setHighlighted:highlighted animated:animated]; if (highlighted) { self.photoTitleLabel.shadowColor = [UIColor darkGrayColor]; self.photoTitleLabel.shadowOffset = CGSizeMake; } else { self.photoTitleLabel.shadowColor = nil; }}
// Define.h@protocol BusinessController <NSObject>@end// ViewController.h@interface ViewController : UIViewController@property (nonatomic, strong) NSMutableArray<id<BusinessController>> *businessControllers;@end

3.3 cell 初始化和更新分离

首先,我们希望能够将 View Controller 的状态事件通知给 Business
Controller ,而 Business Controller
可以选择性的实现这些事件。所以我们先定义一个协议 ViewControllerEvents
。因为是可选择性实现,所以为 optional 。

另外一种好的做法就是将cell的根据model更新的方法,拆分到分类中完成。实际开发中,可能存在复杂的cell代码量很大,此时可以借助分类的方法分离关注点。

// Define.h@protocol ViewControllerEvents <NSObject>@optional- jx_viewDidLoad;- jx_viewWillAppear;- jx_viewDidAppear;- jx_viewWillDisappear;- jx_viewDidDisappear;// ... 其它主框架的事件也可放在这里@end

3.4 简单谈谈MVVM

然后使 BusinessController 遵循 ViewControllerEvents 协议,这样在
BusinessController 就有了监听 VC
事件的能力,并且可以自动补全这些方法名。

1、MVVM本质上也是从MVC中派生出来的思想,由M、V、VM、V四部分组成,主要是为了减少MVC中Controller承担的负荷。2、借助ViewModel可以降低View和Model的耦合度。3、虽然ViewModel是MVVM组成的一部分,但是MVC中依然能用,上述分离tableView的dataSource就是很好的说明。MVVM的关键是要有ViewModel。而不是ReactiveCocoa、RXSwift或RXJava等。4、ReactiveCocoa或RXSwift只是能更好的体现能更好地体现MVVM的精髓。使用函数响应式框架能更好的实现数据和视图的双向绑定(ViewModel的数据可以显示到View上,View上的操作同样会引起ViewModel的变化),降低了ViewModel和View的耦合度。5、ReactiveCocoa或RXSwift不应该因为他本身难以被理解而被神化。通过这两个框架可以实现ViewModel和View的双向绑定,但同样会存在几个比较重大的问题。
首先,ReactiveCocoa或RXSwift的学习成本很高;其次,数据绑定使得 Bug
很难被调试,当界面出现异常,可能是View的问题,也可能是数据ViewModel的问题。而数据绑定会使一个位置的bug传递到其他位置,难以定位;最后,数据绑定是需要消耗更多的内存,对于大型项目更是如此。只是结合自己所学知识谈谈理解,如果对RXSwift感兴趣,推荐这个链接。

// Define.h@protocol BusinessController <ViewControllerEvents>@required// 建立一个vc的弱引用,用于访问vc@property (nonatomic, weak) ViewController *viewController;@end

四、合理拆分模块

接着, VC 需要向业务模块发送这些状态事件。以 viewWillAppear 为例,

4.1 模块拆分大小要合理

// ViewController.m- viewWillAppear:animated { [super viewWillAppear:animated]; [self.businessControllers enumerateObjectsUsingBlock:^(id<BusinessController> _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj respondsToSelector:@selector(jx_viewWillAppear)]) { [obj jx_viewWillAppear]; } }];}

如果模块被拆分的太粗糙,基本就是简单的封装,并没有进一步细化,只是将所有的功能集中在一起,这样做似乎没有太大意义如果模块被拆分的很细,Controller中很执行相关模块的功能就要调用相关模块代码,似乎代码量并不会减少太多。比如在做即时通信应用开发时,支持的消息类型有文字、语音、图片、视频消息。其中后三种消息类型同文字消息不同,后三者要求发送消息的时候,首先要像后台请求上传资源的权限,获取上传资源权限后,返回对应的字段(该字段以实际情况不同,可能是id,也可能是token之类的),上传成功后获取资源对应的URL,再把资源的URL通过类似文字消息的发送方式发送出去。此时,可以拆分成三个模块数据发送、上传资源申请、内容上传。如果要发送文字消息,直接在Controller中调用模块A即可;但是如果想发送其他消息,就要依次调用模块B、模块C、模块A,按照这种调用方式,Controller必然会膨胀。

现在,当我们需要新增一个模块 A 时,只需使其遵循 BusinessController
协议,一切就像在一个全新的 VC 中编码一样,十分清爽。

4.2 策略模式

// A_ControllerClass.m- jx_viewWillAppear { // do some logic request or other business logics ...}

在说合理拆分模块之前,先简单说下策略模式,因为接下来举的例子中涉及策略模式。

最后,我们只需在 VC 中添加各个业务模块,让整个流程跑通:

策略模式一般是指:

// ViewController.m- viewDidLoad { [super viewDidLoad]; [self addBusinessControllers:@[[A_ControllerClass new], [B_ControllerClass new], [C_ControllerClass new], ...]];}
1. 可以实现目标的方案集合;2. 根据形势发展而制定的行动方针和斗争方法;3. 有斗争艺术,能注意方式方法。

至此, VC
中的代码就被我们划分为了许许多多的模块。可是,业务模块之间,是需要通信的,那我们又如何解决这个通信问题呢?我们最容易想到的是两种常规的通信方式——
NSNotification 和 delegate 。

switch,if-else之类的分支语句,此类语句给人的直观感觉是判断条件明确,代码层次清晰,缺点可能是代码繁琐,杂乱无章,而且拆分困难。特别是到后期维护代码的时候,这种状况往往令人有食之无味,弃之可惜的感觉。使用策略模式可以代替switch或if-else之类的代码。

首先, NSNotification 是不合适的。这是一种全局通知,整个 APP
都会收到。我们希望的结果是, ViewController 实例一中的模块 A 给模块 B
发消息时,不会发送到 ViewController 实例二中的模块 B 去。

举个例子,以下是小明的计划安排:

那我们就用 delegate 吧?—— NO! 第一,使用 delegate
我们需要不断的去维护那些对象之间的 delegate 关系(即在 VC 中编写
delegate 的依赖关系,A.delegate = B),这也会引入 Massive View
Controller
中提到的时序问题。第二,若是模块 A 的代理事件模块 B 和模块
C 都需要监听,我们还需要将 delegate 做成数组。咦,真够恶心。

 周一打篮球 周二逛街 周三洗衣服 周四打游戏 周五唱歌 其他休息

所以,我们能否找到一种更好的方式来解决通信问题呢?

借助策略模式我们可以这样实现代码:

这里我提供的解决方案是使用 OC
的消息转发特性(对消息转发不太了解的同学,可以学习一下《Effective
Objective-C 2.0》中消息转发的章节)。首先我们创建一个消息中心
CommunicationCenter ,一个消息协议 BusinessControllerConversation
。让消息中心遵循消息协议,但其内部不实现任何方法,其只做转发,将消息转发给每一个实现了该消息的业务模块。接收消息的
BC 也遵循 BusinessControllerConversation 协议。

@interface XiaoMing : NSObject- doSomethingWithDayStr:(NSString *)dayStr params:(NSDictionary *)paramsDict;@end

#import "XiaoMing.h"@interface XiaoMing()@property(nonatomic,copy)NSDictionary *strategyDict;//策略@property(nonatomic,copy)NSDictionary *paramDict;//参数@end@implementation XiaoMing- doSomethingWithDayStr:(NSString *)dayStr params:(NSDictionary *)paramsDict{ self.paramDict = paramsDict; if (self.strategyDict[dayStr]){ NSInvocation *doWhat = self.strategyDict[dayStr]; [doWhat invoke]; }else{ [self sleep]; }}- (NSInvocation *)invocationWithMethod:selector{ NSMethodSignature*signature = [[self class] instanceMethodSignatureForSelector:selector]; if (signature == nil) { NSString *reason = [NSString stringWithFormat:@"提示:The method[%@] is not find", NSStringFromSelector]; @throw [NSException exceptionWithName:@"错误" reason:reason userInfo:nil]; } NSInvocation*invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = self; invocation.selector = selector; NSDictionary *param = self.paramDict; //index表示第几个参数,注意0和1已经被占用了(self和_cmd),所以我们传递参数的时候要从2开始。 [invocation setArgument:& atIndex:2]; return invocation;}- playBasketball:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- shopping:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- washClothes:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- playGames:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- sing:(NSDictionary *)dict{ NSLog(@"方法:%s 参数:%@",__FUNCTION__,dict);}- sleep{ NSLog(@"这是其他情况:%s",__FUNCTION__);}- (NSDictionary *)strategyDict{ if (_strategyDict == nil) { _strategyDict = @{ @"day1" : [self invocationWithMethod:@selector(playBasketball:)], @"day2" : [self invocationWithMethod:@selector(shopping:)], @"day3" : [self invocationWithMethod:@selector(washClothes:)], @"day4" : [self invocationWithMethod:@selector(playGames:)], @"day5" : [self invocationWithMethod:@selector] }; } return _strategyDict;}@end
// Define.h@protocol BusinessControllerConversation <NSObject>@end// Define.h@protocol BusinessController <ViewControllerEvents, BusinessControllerConversation>@required// 建立一个vc的弱引用,用于访问vc@property (nonatomic, weak) ViewController *viewController;@end// CommunicationCenter.m@interface CommunicationCenter : NSObject <BusinessControllerConversation>// 建立一个vc的弱引用,用于访问vc@property (nonatomic, weak) ViewController *viewController;@end// CommunicationCenter.m- forwardInvocation:(NSInvocation *)anInvocation { SEL selector = anInvocation.selector; [self.viewController.businessControllers enumerateObjectsUsingBlock:^(id<BusinessController> _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj respondsToSelector:selector]) { [anInvocation invokeWithTarget:obj]; } }];}

外部调用可以完全不再使用if-else的判断了。

接着,我们在 VC 中创建并持有一个消息中心。

XiaoMing *xm = [[XiaoMing alloc]init]; //各种情况直接赋值给dayStr即可。NSString *dayStr = @"day3s";[xm doSomethingWithDayStr:dayStr params:@{@"key":@"test"}];
// ViewController.h@property (nonatomic, strong) CommunicationCenter *communicationCenter;// ViewController.m_communicationCenter = [CommunicationCenter new];

4.3 合理应用策略模式和组合方式解决上述4.2问题

这样,当我们的业务模块之间需要通信时,将消息定义在
BusinessControllerConversation
中,然后直接向消息中心发送消息即可。例如当前页面的刷新按钮被点击了,但管理刷新按钮的模块并不管当前页面有哪些模块需要刷新,它只管将该消息抛到消息中心。而需要刷新的业务模块,则实现该消息即可。

关于上述问题我们可以通过组合和策略模式解决。首先创建一个MessageManager类。对外提供的接口大概是这样的:

// Define.h@protocol BusinessControllerConversation <NSObject>@optional- msg_refreshButtonClicked;@end// B_ControllerClass.m- refreshBtnClicked { [self.viewController.communicationCenter msg_refreshBtnClicked];}// A_ControllerClass.m- msg_refreshBtnClicked { // do some business logic ...}
typedef NS_ENUM (NSUInteger, MessageSendStrategy){ MessageSendStrategyText = 0, MessageSendStrategyImage = 1, MessageSendStrategyVoice = 2, MessageSendStrategyVideo = 3}@protocol MessageManagerDelegate<NSObject> @required - messageSender:(MessageSender *)messageSender didSuccessSendMessage:(BaseMessage *)message strategy:(MessageSendStrategy)strategy; - messageSender:(MessageSender *)messageSender didFailSendMessage:(BaseMessage *)message strategy:(MessageSendStrategy)strategy error:(NSError *)error;@end@interface MessageManager:NSObject@property (nonatomic, weak) id<MessageSenderDelegate> delegate;@property(nonatomic,copy)NSDictionary *strategyDict;//主要在这里定义策略,内部通过Invoke唤起对应方法。- sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy;@end

由此,我们实现了单个VC中,模块之间一对多的互相通信。这里值得注意的是,模块
A 和模块 B 的耦合度几乎降至最低。因为 A 和 B
之间互相都不知道对方,不需要设置对方为 delegate
,也不会有建立依赖的时序问题。 BC 都全部面向消息编程,即面向协议编程。

外部调用形式大概是这样的,除此之外还要遵守MessageManagerDelegate协议并实现协议方法。

这就是使用 CommunicationCenter
进行统一转发的通信方式所带来的极大好处:消息发送方不需要关心谁接收消息,其只管通知一下某事件发生了。消息接收方也不需要关心谁发送的消息,其只管接收消息做出反应。这样使业务模块间的耦合性降至最低。

[self.messageManager sendMessage:message withStrategy:MessageSendStrategyText];

不难发现,只要是业务模块 BC 所需要的事件,我们都可以通过
CommunicationCenter 进行转发。所以我们让 CommunicationCenter
BusinessController 遵循 ViewControllerEvents 协议,这样
ViewController 中的状态事件,我们直接抛给 CommunicationCenter
即可。状态事件会经过 CommunicationCenter 路由至业务 BC 。

MessageManager类.m文件实现大概是这样的:

// ViewController.m- viewWillAppear:animated { [super viewWillAppear:animated]; [self.communicationCenter jx_viewWillAppear];}
@interface MessageManager()@end@implementation MessageManager- sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy{ ..... if (self.strategyDict[@]){ NSInvocation *doWhat = self.strategyDict[@]; [doWhat invoke]; } ...... ......}............@end

在采用 BC 的架构之后,所有的模块都需要创建 BC ,再也没有随意散落在 VC
中的代码。

4.4 减少跨层数据交流

至此,我们实现了将 VC
中的业务模块逐一打散,各自为营,也支持业务模块之间的灵活通信。其代码量无限增长的问题、代码糅杂在一起鱼龙混杂的问题等,都得到了解决。

前面注意事项。那么接下来就是要使用模块了,使用模块的时候同样有需要注意的地方:减少跨层数据交流。举个例子,假如有模块一、模块二、模块三,按照正常的调用方式是外部使用模块一调用模块二的方法,模块二的方法再调用模块三的方法。但是随着模块功能的完善,突然有一天出现模块一直接调用模块三的情况,那么后续就很难避免其他开发人员可能直接拿模块一调用模块三方法。类似这种跨层的数据交流很不利于项目的后续维护。转自

图片 5BC_Overview.png

BC 设计模式的通信结构如上图所示( Owner 即文中的 ViewController )。
Owner 将主流程事件发至消息中心,由消息中心路由至各个 Module 。而各个
Module 之间也通过消息中心转发至其他 Module 。

可以看到 BC 和传统的 MVC , MVVM , VIPER 的关系不是互斥的,是并存的。从
MVC 到 MVVM 到 VIPER 是对架构的不断细化。而 BC
则是提供了一种划分模块的机制。即一个 Module 可以是 Model ,可以是 View
,也可以是包含了 MVC 的一个完整的模块。在使用 MVC , MVVM , VIPER
等设计模式时,我们可以同时使用 BC 来帮助我们组织各个模块。通过 BC
,我们将根据不同架构设计的不同模块有机的结合了起来。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图