iOS架构师之路:控制器(View Controller)瘦身设计
前言
古老的MVC架构是容易被iOS开发者理解和接受的设计模式,但是由于iOS开发的项目功能越来越负责庞大,项目代码也随之不断壮大,MVC的模糊定义导致我们的业务开发工程师很容易把大量的代码写到视图控制器中,行业中对这种控制器有个专业词汇Massive ViewControler(臃肿的视图控制器)。代码臃肿导致可读性可维护性差,而且这种不清晰的设计还有许多的副作用,比如代码重用性差。作为架构师需要关注项目的代码质量。指导业务开发工程师写出高质量,高健壮性,高可用的代码也是很重要的工作。因此需要知道一些为控制器瘦身的技巧,并在项目中帮助业务开发工程师合理的运用它们。本文翻译一篇国外优秀文章:Lighter View Controllers。
示例代码下载地址:JackieHoo's GitHub
分离数据源(Data Source)等协议(Protocol)
瘦身控制器的有效方法之一就是将实现 UITableViewDataSource 协议相关的代码封装成一个类(比如本文中的 ArraryDataSource )。如果你多用几次这个设计,你就会创建复用性高的封装类。
举个例子,示例工程中的类 Photos控制器实现如下数据源方法:
# pragma mark Pragma
- (Photo*)photoAtIndexPath:(NSIndexPath*)indexPath {
return photos[(NSUInteger)indexPath.row];
}
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
return photos.count;
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
PhotoCell* cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier
forIndexPath:indexPath];
Photo* photo = [self photoAtIndexPath:indexPath];
cell.label.text = photo.name;
return cell;
}
上面示例的数据源的实现都与 NSArray 有关,还有一个方法的实现与 Photo 有关(Photo 与 Cell 呈一一对应关系)。下面让我们来把与 NSArray 相关的代码从 控制器中抽离出来,并改用 block 来设置 cell 的视图。当然你也可以用代理来实现,取决于你的个人喜好。
@implementation ArrayDataSource - (id)itemAtIndexPath:(NSIndexPath*)indexPath {
return items[(NSUInteger)indexPath.row];
} - (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
return items.count;
} - (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
id cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];
id item = [self itemAtIndexPath:indexPath];
configureCellBlock(cell,item);
return cell;
} @end
现在我们可以控制器中的三个数据源代理方法可以干掉,并且把 控制器的 dataSource 设置为 ArrayDataSource 的实例。
void (^configureCell)(PhotoCell*, Photo*) = ^(PhotoCell* cell, Photo* photo) {
cell.label.text = photo.name;
}; photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
cellIdentifier:PhotoCellIdentifier
configureCellBlock:configureCell];
self.tableView.dataSource = photosArrayDataSource;
通过上面的方法,你就可以把设置 Cell 视图的工作从 控制器中抽离出来。现在你不需要再关心indexPath如何与 NSArrary 中的元素如何关联,当你需要将数组中的元素在其它 UITableView 中展示时你可以重用以上代码。你也可以在 ArrayDataSource 中实现更多的方法,比如tableView:commitEditingStyle:forRowAtIndexPath:。
这样做还能带来额外的好处,我们还可以针对这部分实现编写单独的单元测试。不仅仅针对NSArray,我们可以使用这种分离思路处理其他数据容器(比如NSDictionary)。
该技巧同样适用于其他 Protocol ,比如 UICollectionViewDataSource 。通过该协议,你可以定义出各种各样的 UICollectionViewCell 。假如有一天,你需要在代码在使用到 UICollectionView 来替代当前的 UITableView,你只需要修改几行 控制器中的代码即可完成替换。你甚至能够让你的 DataSource 类同时实现 UICollectionViewDataSource 协议和 UITableViewDataSource 协议。
把业务逻辑移至 Model
下面是一段位于 控制器中的代码,作用是找出针对用户active priority的一个列表。
- (void)loadPriorities {
NSDate* now = [NSDate date];
NSString* formatString = @"startDate <= %@ AND endDate >= %@";
NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate];
self.priorities = [priorities allObjects];
}
然而,假如你把代码实现移至 User 的 Category 中,控制器中的代码将会更简洁、更清晰。
将以上代码移到User+Extension.m中
- (NSArray*)currentPriorities {
NSDate* now = [NSDate date];
NSString* formatString = @"startDate <= %@ AND endDate >= %@";
NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];
}
ViewController.m 中的代码可以改成这个鬼样子,是不是明显要简洁许多,可读性强很多呢。
- (void)loadPriorities {
self.priorities = [self.user currentPriorities];
}
实际开发中,有些代码很难移至 model 对象中,但是很明显这些代码与 model 对象有关。针对这种情况,我们可以创建一个 store 类,并把相关代码迁移进去。
创建 Store 类
在这个示例项目工程中,我们有一段用于从本地文件加载数据并解析的代码:
- (void)readArchive {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSURL *archiveURL = [bundle URLForResource:@"photodata"
withExtension:@"bin"];
NSAssert(archiveURL != nil, @"Unable to find archive in bundle.");
NSData *data = [NSData dataWithContentsOfURL:archiveURL
options:
error:NULL];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
_users = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"users"];
_photos = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"photos"];
[unarchiver finishDecoding];
}
控制器不应该负责以上的工作,控制器只要负责数据调度就可以了,数据获取的工作我们完全可以交给 store 对象来负责。通过将这些代码从 控制器中抽离出来,我们可以更容易复用、测试这些方法、同时让控制器变得更轻巧( Store 对象一般负责数据的加载、缓存、持久化。Store 对象也经常被称作 Service Layer 对象,或者 Repository 对象)。
将 Web Service 逻辑移至 Model 层
这与上一个主题非常相似:别把 Web Service 相关的代码写在 控制器中,应该把这部分代码抽离出来。并通过方法的回调对数据进行处理。
不仅如此,你还可以把处理异常情况的工作也转交给 Store 对象负责。
把视图相关的代码移至 View 层
同样构建视图(尤其是复杂视图)的代码也不应该写在 View Controller (关我毛事啊,我只负责调度和通信啊)中。要么使用Interface Builder ,要么封装一个 Vew 的子类来完成这部分工作。假设现在需要实现自定义一个日期选择器。我们应该新建一个 DatePickerView 的子类来完成构建视图的工作,而不是把这部分工作放在 View Controller 中完成。同样的,这将是你的代码更简洁,复用性更强。
除了用 code 的形式来实现自定义视图,你也可以使用 Interface Builder 来完成构建自定义视图的工作。很多人都认为 Interface Builder 只能用于为 View Controller 构建视图,其实不然,你可以通过单独的 nib 文件来加载在 Interface Builder中构建的自定义视图。在示例工程当中,我们创建了一个包含了 Photo Cell 视图的 PhotoCell.xib 文件。
如图所示,我们在 view 中创建了属性(无需设置 File’s Owner 对象)并把它们与 Interface Builder 中的视图关联起来。这个方法同样适用于构建其它自定义视图。
通讯
我们在控制器中经常需要与其它控制器、Model 、 View 进行通讯。虽然这本来就是 控制器应该负责处理的事情,但我们依然可以用尽可能少的代码完成我们控制器的负责的工作。
现在已经有很多成熟的方案来建立 控制器与 View 的通讯(例如 KVO 和 fetched results controllers)。然而 控制器之间的通讯目前还没有类似的方案可以借鉴。
在实际开发中,我们经常需要把 遇到需要把控制器持有的一些状态信息,传递到 多个 控制器的需求。通常我们会将这些状态信息保存在一个对象中然后传递给其他的视图控制器。这部分的瘦身技巧比较复杂,我留在以后再专门讲解吧。
结论
我们已经展示了一些瘦身控制器的方法。作为架构师我们不可能完全照搬这些设计技巧,但我们需要清楚我们这么做的目的,我们只有一个目标:使得代码更易于维护,只要架构师在review代码时时刻关注这个目标,我们可以就可以扩展这些技巧,灵活运用到项目中。通过了解这些方法,我们能够更好的处理好复杂的视图控制器,并且让这些视图控制器的代码更整洁,更清晰。
欢迎关注我的微信公众号:丁丁的coding日记
iOS架构师之路:控制器(View Controller)瘦身设计的更多相关文章
- iOS架构师之路:慎用继承
最近在看大神Casa的文章<跳出面向对象思想(一) 继承>,脑洞大开.文章给我们展示了一个随着产品需求不断变化的例子,该例子中通过继承实现不同页面的搜索视图和搜索逻辑的代码复用,随着产品需 ...
- IOS架构师之路:我对IOS架构的点点认识(大纲)
1.今天我鼓起了勇气,想纪录自己对IOS架构学习成长的点点滴滴. 从事IOS开发也有几年的时间,从刚開始最主要的语言.界面.逻辑,再到后面复杂点的线程.数据处理.网络请求.动画,最后到最复杂的底层音视 ...
- android大牛高焕堂最新力作-android架构师之路
android大牛高焕堂 个人介绍: Android专家顾问,台湾Android论坛主席,现任亚太地区Android技术大会主席,台湾Android领域框架开发联盟总架构师.发表100多篇Androi ...
- 架构师之路-在Dubbo中开发REST风格的远程调用
架构师之路:从无到有搭建中小型互联网公司后台服务架构与运维架构 http://www.roncoo.com/course/view/ae1dbb70496349d3a8899b6c68f7d10b 概 ...
- 【转】java架构师之路:JAVA程序员必看的15本书的电子版下载地址
作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...
- paip.java 架构师之路以及java高级技术
paip.java 架构师之路以及java高级技术 1. Annotation 设计模式... 概念满天飞.ORM,IOC,AOP. Validator lambda4j memcache. 对 ...
- Java架构师之路:JAVA程序员必看的15本书
作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...
- 高焕堂《android从程序员到架构师之路》 YY讲坛直面大师学习架构设计
<android从程序员到架构师之路>YY讲坛活动: sundy携手高焕堂老师全程YY答疑 与大师一起,分享android技术 时间:7月21日下午2:00 报名联系QQ:22243 ...
- 基于libevent, libuv和android Looper不断演进socket编程 - 走向架构师之路 - 博客频道 - CSDN.NET
基于libevent, libuv和android Looper不断演进socket编程 - 走向架构师之路 - 博客频道 - CSDN.NET 基于libevent, libuv和android L ...
随机推荐
- ROS学习笔记(五)——建立工作空间
pre.ctl { font-family: "Liberation Mono", monospace } p { margin-bottom: 0.25cm; line-heig ...
- PBS 安装
How to install PBS Pro using the configure script. . Install the prerequisite packages for building ...
- elk
http://467754239.blog.51cto.com/4878013/1700828/
- 使用vs中的发布功能发布asp.net core项目时遇到ERROR_CERTIFICATE_VALIDATION_FAILED错误
今天将VS2015编制的一个asp.net core项目发布到服务器进行测试,使用的是vs中主菜单"生成"中的"发布"功能. 遇到了一个错误,在网上反复检索尝试 ...
- Python正则式的基本用法
Python正则式的基本用法 1.1基本规则 1.2重复 1.2.1最小匹配与精确匹配 1.3前向界定与后向界定 1.4组的基本知识 2.re模块的基本函数 2.1使用compile加速 2.2 ma ...
- bootstrap中如何让响应式图片(img-responsive)水平居中
我们在用bootstrap排版内容的时候,有的时候在内容中需要图片水平居中对齐. 一般情况下,我们的图片都使用了 .img-responsive 类来实现响应式图片.如果需要实现响应式图片水平居中,那 ...
- C语言实现四则运算
学生:宋丹丹 张潇裕 #include<iostream>#include<ctime>using namespace std;void main(){ int x1,x2,a ...
- FastReport 自定义数据集
1.可以自定义一个新的数据源 参考: internal class VirtualDataSource : DataSourceBase { private int FVirtualRows ...
- 常用的web功能测试方法
功能测试就是对产品各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求功能,即是否满足需求.常用的测试方法如下: 1.页面连接检查:每一个连接是否都有对应的页面,并且页面之间切换正确. ...
- HashMap & HashTable的区别
HashMap & HashTable的区别主要有以下: 1.HashMap是线程不安全的,HashTable是线程安全的.由这点区别可以知道,不考虑线程安全的情况下使用HashMap的效率明 ...