本文做为 QQ 阅读 7.0 改版总结,从架构、页面元素模块化、UI 组件化、基于 iOS 系统响应链的事件处理、业务模板化等方面阐述了一套高效的列表类应用开发解决方案。git
本文同时发表于个人我的博客github
QQ 阅读迎来了7.0版本,做为惯例大版本须要大动做——『UI大改版』。 本文主要是对此次改版的一个总结并提炼出一套通用的『列表类业务』开发解决方案。 本文将从如下几个方面展开讨论:算法
本文部份内容来自列表类应用场景模板化和自定义 UI 组件库网络
列表类业务应该说是大多数 App 的主要业务场景,如朋友圈、新闻类 App 首页、各种个性化推荐页、微博首页以及咱们的书城等等。架构
列表类业务其流程主要是:模块化
UITableView
、UICollectionView
)形式展现出来;对于列表类业务每一个项目团队可能都有一套架构,在 QQ 阅读不断迭代的过程当中也演化出一套架构。 组件化
MVC 模式饱受诟病的一点就是:Controller 常常会变得过于臃肿(Massive View Controller)。 为了解决这一问题,业界提出了多种解决方案,大部分都是经过添加中间层,将 Controller 的功能分解到中间层上,如 MVP (Model View Presenter) 模式。布局
为了解决 Controller 臃肿问题,在咱们的架构中将页面元素抽象成一个个的 Module。 动画
在 TableView 中一个 Module 对应一个 section。 Module 的职责主要有:ui
UITableViewDataSource
协议);——即负责『模块』的全部逻辑(与 React Component 相似)。
经过上述分析可知,Module 解析、存储业务数据,Manager 存储、管理 Module。
这种作法也存在弊端,因为将解析业务数据、控制 UI 展现的逻辑(建立 cell 等)都放在了 Module 中。使得 Module 违反了『单一职责原则』。
『单一职责原则』(SRP)做为面向对象设计的五大原则『SOLID』之一,很容易理解,也很难把握!『就好像生活中的各类"适量",适量放点盐、适量加点水…』 Bob大叔在《敏捷软件开发》中,将类的单一职责原则描述为『应该仅有一个引发它变化的缘由』。
在 Module 中,业务数据解析、UI 展现就是两个可变的因素——『一样的 UI 用于展现不一样的网络协议返回的数据、同一协议返回的数据展现为不用的 UI』。 在 QQ 阅读中,书籍列表页就属于『一样的 UI 展现不一样协议返回的数据』:
『敏捷开发』的原则之一就是尽可能保持代码简单、并在必要时进行重构,防止代码变坏。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
QRBaseModule *module = [self.manager moduleAtIndex:indexPath.section];
return [module heightForRow:indexPath.row];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
QRBaseModule *module = [self.manager moduleAtIndex:section];
return [module numberOfRows];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
QRBaseModule *module = [self.manager moduleAtIndex:indexPath.section];
return [module cellForRow:indexPath.row tableView:tableView];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [self.manager moduleCount];
}
复制代码
如上述代码,模块化后UITableViewDelegate
、UITableViewDataSource
的大部分方法都转发给相应的 Module 去处理,大大简化了 Controller 的复杂度。
另外,在页面上增删任何元素都只需在 Manager 中增删相应的 Module 便可,Controller 无需任何改动——在 Controller 层面遵照了开放-封闭原则『OCP』。
模块化不只简化了 Controller,同时也提升了代码的复用性。Module 能够在不一样页面间复用。若是这些逻辑所有放在 Controller 里,基本没有复用性可言。
模块化有没有缺点? 答案是确定的😒:
UITableViewDelegate
、UITableViewDataSource
的部分方法);固然啦,我的认为利大于弊😊
QQ 阅读7.0改版,UI 修改的工做量占大头,涉及200多个页面的修改。 此时,充分体现出 UI 复用的重要性。
虽然,咱们很早就提出经过 View-ViewModel 的方式实现 UI 组件化,提升复用性。 遗憾的是,因为历史缘由,在咱们的工程中依然存在大量重复的实现,即『同一 UI 样式,N 份实现』。这对于 UI 大改版是灾难性了!——「不只工做量成倍增长,还有漏改的可能性」
为了不灾难再次上演(8.0、9.0...),这次改版过程当中,咱们严格要求全部 UI 都必须以 View-ViewModel 模式作成 UI 组件。
在继续以前,咱们简单描述一下什么是 UI 组件:
同时,咱们将 UI 组件分为外部 UI 组件、内部 UI 组件:
QRExternalUIComponent
协议,使得业务方可灵活控制其边距;@protocol QRExternalUIComponent <NSObject>
- (void)setEdgeInsets:(UIEdgeInsets)edgeInsets;
@end
复制代码
复用没把握好火候就变成耦合了。
例1.
if...else...
区分,这就是严重的耦合,给后面的维护形成很大的困难。
例2.
if...else...
区分,内部还要处理六书、八书的状况,还要兼容 iPad,内部实现异常复杂,致使你们都不敢去碰这块代码。
为此,咱们制定了以下规则:
Template Method: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure. 此例中 UI 布局就是 Template Method 中的算法结构,而布局的细节则能够经过子类去控制。
咱们常常吐槽 QQ 阅读 UI 的多样性在业界能排 Top1。
好了,下面进入本节的正题,如何构造出复用性高的 UI 组件。
高可复用的 UI 组件,至少要知足如下两点:
总之,UI 组件要与业务解耦。 此时,MVVM 模式进入咱们的视线,在该模式中 ViewModel 的存在是否是很好的解决了上面的问题。 在 MVVM 模式中,ViewModel 向上为 View 提供展现数据(该数据已经在 ViewModel 中处理好了,View 无需任何处理,只要展现便可),向下接收来自业务层的数据,处理相关的业务展现逻辑。
能够看出,ViewModel 做为中间层很好地将业务与 UI 隔离开。 说到 MVVM,不少同窗并不喜欢,以为其中的 Data-Binding 很麻烦,但咱们构建 UI 组件时用到的是 View-ViewModel 结构,并不要求必定是 MVVM,在 MVC 等模式下也可以使用。
同时,咱们采用的是面向接口的模式,View 对外依赖的是接口(protocol),而不是某个具体的 ViewModel。每一个 UI 组件其结构以下:
ViewModelProtocol
协议为 View 提供数据。
如上文提到的单书 UI,咱们抽取为一个组件QRLeftPictureRightTextView
:
Module 与 UI 组件在两个不一样的层面实现复用。
现有的事件处理方案有两大痛点,因而提出了基于响应链『Chain of Responsibility』的事件处理方案。
cell.delegate = controller;
view.delegate = cell;
…
复制代码
尤为是第一点一直困扰着我。直到前不久在《Design Patterns》一书中看到在介绍『Chain of Responsibility』模式时的一句话:『Using existing links works well when the links support the chain you need. It saves you from defining links explicitly, and it saves space』。 UIResponder
中的 nextResponder
不正是这个『existing links』吗! 最上层 View 的事件经过nextResponder
链就能够顺利传到 ViewController 中,从而也就省去了 delegate 的逐级传递了,痛点一、2随之化解。 为此,咱们为 UIResponder
添加了传递、处理事件的分类:
@protocol ZSCEvent <NSObject>
@property (nonatomic, strong) __kindof UIResponder *sender;
@property (nonatomic, strong) NSIndexPath *indexPath;
@property (nonatomic, strong) NSMutableDictionary *userInfo;
@end
@interface UIResponder (ZSCEvent)
- (void)respondEvent:(NSObject<ZSCEvent> *)event;
@end
@implementation UIResponder (ZSCEvent)
- (void)respondEvent:(NSObject<ZSCEvent> *)event
{
[self.nextResponder respondEvent:event];
}
@end
复制代码
UIResponder
的实现只是简单地将事件传递给nextResponder
。 因为 View 不包含业务数据,因此事件传递的过程当中须要不断添加一些信息。
所以,咱们将
ZSCEvent#userInfo
定义为 mutable。正常状况下外露接口通常都是 immutable。
@implementation UITableViewCell (ZSCEvent)
- (void)respondEvent:(NSObject<ZSCEvent> *)event
{
event.sender = self;
[self.nextResponder respondEvent:event];
}
@end
复制代码
如,在UITableViewCell
的respondEvent:
中须要将sender
设置为self
,以便在UIViewController
中能够经过cell
找到对应的 Module。
- (void)respondEvent:(NSObject<ZSCEvent> *)event
{
NSAssert([event.sender isKindOfClass:UITableViewCell.class], @"event sender must be UITableViewCell");
if (![event.sender isKindOfClass:UITableViewCell.class]) {
return;
}
NSIndexPath *indexPath = [_tableView indexPathForCell:event.sender];
id<ZSModule> module = [self.manager moduleAtIndex:indexPath.section];
event.sender = self;
event.indexPath = indexPath;
[event.userInfo setObject:_tableView
forKey:ZSCEventUserInfoKeys.tableView];
[module handleEvent:event];
}
复制代码
在 View 中的事件处理代码能够这样:
- (void)_clickedButton:(id)sender
{
ZSCEvent *event = [[ZSCEvent alloc] init];
event.sender = self;
[event.userInfo setObject:@(YES) forKey:@"clickedButton"];
[self respondEvent:event];
}
复制代码
若是一个 cell 中有多个事件须要处理,就须要在
userInfo
中加以区分,如上面代码第5
行。
总之,经过UIResponder
的nextResponder
响应链,没必要再在 view 的层级间传递 delegate,减小了琐碎的代码,提升了开发效率。同时也统一规范了事件处理方案。
列表类应用场景模板化一文对此有详细的描述,在此就不赘述了。 其效果仍是不错的。 不少二级页,因为 Module 是彻底复用的,经过模板化脚本半小时就能作好一个二级页✌️。
简单、高效一直是软件开发、工程管理追求的目标,本文从实际项目经验出发,从架构、解耦、复用等角度总结出一套开发解决方案。