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:0
                                           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 中的视图关联起来。这个方法同样适用于构建其它自定义视图。

通讯

我们在控制器中经常需要与其它控制器ModelView 进行通讯。虽然这本来就是 控制器应该负责处理的事情,但我们依然可以用尽可能少的代码完成我们控制器的负责的工作。

现在已经有很多成熟的方案来建立 控制器View 的通讯(例如 KVOfetched results controllers)。然而 控制器之间的通讯目前还没有类似的方案可以借鉴。

在实际开发中,我们经常需要把 遇到需要把控制器持有的一些状态信息,传递到 多个 控制器的需求。通常我们会将这些状态信息保存在一个对象中然后传递给其他的视图控制器。这部分的瘦身技巧比较复杂,我留在以后再专门讲解吧。

结论

我们已经展示了一些瘦身控制器的方法。作为架构师我们不可能完全照搬这些设计技巧,但我们需要清楚我们这么做的目的,我们只有一个目标:使得代码更易于维护,只要架构师在review代码时时刻关注这个目标,我们可以就可以扩展这些技巧,灵活运用到项目中。通过了解这些方法,我们能够更好的处理好复杂的视图控制器,并且让这些视图控制器的代码更整洁,更清晰。

欢迎关注我的微信公众号:丁丁的coding日记

时间: 2024-12-25 13:17:50

iOS架构师之路:控制器(View Controller)瘦身设计的相关文章

IOS架构师之路:我对IOS架构的点点认识(大纲)

1.今天我鼓起了勇气,想纪录自己对IOS架构学习成长的点点滴滴. 从事IOS开发也有几年的时间,从刚開始最主要的语言.界面.逻辑,再到后面复杂点的线程.数据处理.网络请求.动画,最后到最复杂的底层音视频.图像算法.自己定义各种效果.网络底层处理.甚至是最后的性能:neon.asm优化. 感觉自己在IOS的开发中,每次都是雾里看花,明明非常接近真理却总是触摸不到.对IOS缺乏一种全局把控的感觉.所以我下定决定想看看IOS的一些官方文档,看看IOS的各个模块的层次结构究竟是怎么回事. 大约从一年前開

Java架构师之路:从Java码农到年薪八十万的架构师,最牛Java架构师进阶路线

从Java码农到年薪八十万的架构师,资深架构师大牛给予Java技术提升学习路线建议,如何成为一名资深Java架构师? 对于工作多年的程序员而言,日后的职业发展无非是继续专精技术.转型管理和晋升架构师三种选择.架构师在一家公司有多重要.优秀架构师需要具备怎样的素质以及架构师的发展现状三个方面来分析 程序员如何才能晋升为优秀的高薪架构师? 希望通过本文让程序员们了解架构师的市场行情,了解架构师的发展前景,并帮助你更清晰地做出职业规划. 架构师在一家公司有多重要 架构师在公司中担当着「IT架构灵魂人物

架构师之路虽难,但不要放弃!

由于昨天深夜学习不在状态,问责自己的话,分享给那些一直坚持这条道路而又有感到迷茫的小伙伴们! 致深夜迷茫的自己:    北京的压力真的很大吗?你自己的压力真的就那么大吗?真的很累吗?难道想放弃了?心甘情愿的去做一个平庸的人吗?还记不记得当初有那么多瞧不起你的眼神,难道你忘了吗?你知道吗,在你的老师眼里你是个很有能力的学生,每个老师都愿意把最高的分数给你,这是为什么?因为那时候你有一颗战无不胜.不服输的心!现在是怎么了,学不进去了吗?累?熬不下去了?刚刚接手公司中心服务器集群,就感觉累了?你知道吗

Android架构师之路-架构师的决策

android架构师之路-架构师的决策 内涵+造型:可能大部分人对这个内涵和造型不是很理解,在这里我可以给大家举个生动的例子:相信很多人都有自己的汽车, 我们总结汽车有哪些属性和功能,这些都是内涵,大自然中的每个对象都有自己的内涵(人有手有脚,还可以跑),然后我们 将这些内涵放入指定的造型中,类似模版,比如java语言如果定义一个class的时候,必须在作用域(大括号内部)指定属性和 函数,这个class的定义规范就是一个造型,然后我们将汽车这个内涵按照class的规范定义一个汽车class,那

2015重磅炸弹——【视频】Android从程序员到架构师之路

眼看2015年一月份就要接近尾声了,今年的开年第一颗炸弹也该引爆了! Android从程序员到架构师之路,高焕堂老师主讲,总共234节课. 为了方便大家观看,直接传了MP4格式的视频文件,不想下载的朋友可以在线观看. 链接: http://pan.baidu.com/s/1qW1B9mO 密码: sf79 望支持,谢谢!

java架构师之路:JAVA程序员必看的15本书的电子版下载地

转自:http://www.shangxueba.com/faq/view376.html 作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水平的Java程序员们. 一.Java编程入门类 对于没有Java编程经验的程序员要入门,随便读什么入门书籍都一样,这个阶段需要你快速的掌握Java基础语法和基本用法,宗旨就是“囫囵吞枣不求甚解”,先对Java

[Android]Android从程序员到架构师之路的一些笔记

高焕堂老师的讲得不错 //EIT造型 E基类   I接口    T(基类的子类)实现接口 [Android]Android从程序员到架构师之路的一些笔记

测试专家讲述通往测试架构师之路

随着对测试这个职业的了解越来越深,对微软测试技术的掌握越来越多,慢慢地,人就开始对那些测试“大牛”在做什么感兴趣了.他们就是那些在公司内部挂着“测试架构师”头衔的一小撮人. AD: WOT2014:用户标签系统与用户数据化运营培训专场 在公司呆了有几个年头了.在测试技术方面的技能长进了不少,又能享受写代码的乐趣,同事们经常交流对软件测试技术的见解,也在项目中实现一些创新的测试技术和基于自己的想法设计好的测试框架,每天过的很开心.随着对测试这个职业的了解越来越深,对微软测试技术的掌握越来越多,慢慢

深入大数据架构师之路,问鼎40万年薪视频教程

38套大数据,云计算,架构,数据分析师,Hadoop,Spark,Storm,Kafka,人工智能,机器学习,深度学习,项目实战视频教程 视频课程包含: 38套大数据和人工智能精品高级课包含:大数据,云计算,架构,数据挖掘实战,实时推荐系统实战,电视收视率项目实战,实时流统计项目实战,离线电商分析项目实战,Spark大型项目实战用户分析,智能客户系统项目实战,Linux基础,Hadoop,Spark,Storm,Docker,Mapreduce,Kafka,Flume,OpenStack,Hiv