Self-Manager模式在业务开发中的组件化应用

Self-Manager直译就是自管理,第一次听说是在孙源的博文中,基于业务的行为一致,并且具有高复用性,可以将其进行自管理,这种模式称为Self-Manager模式。作者还举了两个例子,其中用户头像的例子在业务开发中会经常遇到,统一的圆角设计、统一的边距设计、统一的点击跳转等等,通过提供分类这种AOP思想让这个用户头像视图自己决定样式以及行为。

在常规的MVC开发中,会将子视图的样式在自身完成,而行为则上报给上层来处理,这个上层更多的是视图控制器。但是在多地方对视图的使用会使得业务代码很臃肿,行为要一次次的传递个上层,有的甚至要穿越好几层视图。而为这种行为统一的视图提供自己处理事件的能力,能够很好的解决这样的问题,这种思想也很契合组件化的初衷。

什么是组件

关于组件化的定义,各种百科上都有解释,我个人理解的是将一些具备相同特性的事物放置在一起,并且其拥有很强的重用性。

这样解释很容易和模块化混淆,毕竟组件化和模块化是会经常放在一起讨论的。模块化更多的是将事物进行拆分,不论是业务还是技术上,将一些具有强相关逻辑的事物进行模块划分,或者按照职责进行划分,这样能够更好的做到开发维护工作。组件化的粒度会小一些,侧重可复用性,通过对组件的组合可以完成具体的业务,模块中会包含一些组件。一个例子就是,我们可以把UIImage、NSString、NSStream等这些当做是不同的组件,而将AVKit、MapKit等等当做是模块,这样理解组件和模块就会很清晰了。

要使得项目更加健壮需要在前期就将具体的业务进行拆分,整体方向上的就是业务和功能模块的拆分,比如直播业务中的开播模块、IM模块、直播间列表模块、礼物模块等等。而在具体方向的拆分上会涉及到网络请求、数据缓存、Toast展示等等。

有了以上关于Self-Manager、组件、模块的一些认识之后,下面来讨论一下具体业务中的一些应用,我会以一个直播业务中首页展示列表的需求来讨论组件化方案。

IGListKit

至于IGListKit的使用不是本文的重点,只简单的做一下使用示例,具体可以参考对应的官方示例。

在IGListKit中会将每一个模块中具有相同特性的section使用IGListSectionController来抽象。在IGListKit中IGListSectionController可以决定当前section中用于显示的视图cellcell的个数以及cell视图的点击事件,甚至当前section的尾部视图等等。这些配置都可以在一个IGListSectionController中完成设置,就可以认为IGListSectionController是一个具备Self-Manager的组件。

IGListKit中对IGListSectionController的使用依赖于一个中间Model,可以将它称为sectionModel,这个sectionModel的作用可以当做是该section的数据抽象,它可以提供一个数组,来决定当前section下有多少数据。由于这个sectionModel主要是提供一个数组,就和平常开发中使用一个数组来表示当前section下有多少数据的方式一样,只不过平时可能会直接使用数组,这里是再抽象一层,通过再抽象一层并为IGListSectionController设置约束,协议约束,可以在其核心功能中起作用。

在IGListSectionController中可以看做一个微型的MVC结构,从名字上看IGListSectionController就是Controller,Model就是为其提供数据的sectionModel,要展示的视图cell就是View,在IGListSectionController内部会将数据和视图进行绑定,这样就将对一个UICollectionView的展示抽象成了几个IGListSectionController。

以上就是IGListKit的简单原理介绍,下面开始讲解一下实现一个可配置、高内聚的model-component组件。

LiveModule

这里没有在IGListKit的基础上实现该方案,主要是由于以下原因:

  • IGListKit中由于具备diff的特性,需要为每一个IGListSectionController配置一个sectionModel,由于diff这个功能目前我们没有用到,这就决定了IGListKit中将有一半的功能都用不上

拆分

在拆分模块的时候,基于我们具体的业务,一共抽离出来ModuleDataSourceComponentLayout四个模块。

Module表示一个具体的业务,比如首页的一个分类,Component表示某一些具备特性的集合,同时也是组成Module的组件,DataSource和Layout是功能类,分别用来管理数据源和计算布局。四者之间的关系为:Component通过DataSource被Module管理,Layout通过Component为DataSource提供布局的计算操作。

Module是对一个具体业务的抽象,可以看做是对UIViewController的解耦,通过将相同的逻辑进行整合:refreshloadMorerequestemptyView。Module的子类只需要实现以下方法,提供一个网络请求类,然后在请求成功的方法中解析数据,添加具体的Component即可:

1
2
3
- (__kindof YTKRequest *) fetchModuleRequest; 

- (void) parseModuleDataWithRequest:(__kindof YTKRequest *)request;

DataSource不仅担任着为Module管理Component的任务,其还实现了UICollectionViewDataSource和UICollectionViewDelegate协议,使用管理的Component为UICollectionView提供数据源,使用每一个Component的Layout为UICollectionView提供UI布局,内部参考IGListKit,使用NSMutableSet来保存注册的Cell和SupplementaryView,这样在Component就可以直接使用其dataSource的一系列dequeueReusable…方法获取对应的cell或者supplementaryView:

1
2
3
4
5
6
7
8
- (kindof UICollectionViewCell *) collectionView:(kindof UICollectionView *)collectionView dequeueReusableCell:(NSMutableSet *)registeredCellIdentifiers withReuseIdentifier:(NSString *)reuseIdentifier cellClass:(Class)cellClass atIndexPath:(NSIndexPath *)indexPath{

if (![registeredCellIdentifiers containsObject:reuseIdentifier]) {
[registeredCellIdentifiers addObject:reuseIdentifier];
[collectionView registerClass:cellClass forCellWithReuseIdentifier:reuseIdentifier];
}
return [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
}

Component只要DataSource为其提供注册好的可复用的Cell和SupplementaryView,因此两者的联系是通过QLLiveModuleDataSourceAble协议弱化了具体类,Component的子类只需要重写-cellForItemAtIndex:方法就可以选择使用何种cell,以及将具体索引下的数据交给cell来更新界面:

1
2
3
4
5
6
// in some subclass of Component .m 
- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index{
YYYOneCCell * ccell = [self.dataSource dequeueReusableCellOfClass:YYYOneCCell.class forComponent:self atIndex:index];
[ccell setupWithData:[self dataAtIndex:index]];
return ccell;
}

通过Module传递给DataSource,DataSource传递给Component,Component传递给Layout,四者通过一个Environment共用UICollectionView和UIViewController,因此在Component中可以使用UIViewController来做一些业务,这一点是和IGListKit一致的,并且在Layout中也可以根据UICollectionView来进行内容范围大小的获取。

Layout中根据insetslineSpacinginteritemSpacingdistributionitemRatio会计算出来具体的itemSize,并且根据index进行缓存,每一个Component可以设置自己的Layout,这些设置会在DataSource中作为UICollectionView的数据源和代理进行布局使用。其中distribution和itemRatio分别表示一屏横向上可以显示的个数以及cell的宽高比,同时为具备灵活性,还提供了QLLiveComponentLayoutDelegate来让Component进行自定义itemSize。

Orthogonal Scroll

常规的垂直方向展示功能已经不能满足多变的需求,像App Store中那种既可以纵向滑动又可以横向滑动的效果越来越受欢迎。在开发实现中更多使用的是在需要横向滑动的cell中添加一个UICollectionView子视图,然后由这个cell来实现具体的数据源和代理,略显麻烦。

在iOS12之后,系统提供了一个关于UICollectionView的layout:UICollectionViewCompositionalLayout,这个layout可以设置丰富多样的布局样式,但是由于系统的限制,iOS12以下的系统不能使用,这在实际业务开发中就决定了这个功能组件暂时不会被大面积使用。不过,好在社区中有根据系统的API自己实现了一套iOS12以下也可以用的IBPCollectionViewCompositionalLayout,由于这个Layout功能太多,并没有直接拿来用,这里只是参考了其中的一些实现逻辑,借鉴的就是实现OrthogonalScroll效果的部分。

这里使用了一个巧方法做了嵌套CollectionView原始CollectionView之间的数据源和代理的转换,使得在一个横向滑动的Component中配置cell和在纵向的Component中配置cell看起来是一样的。

对Component的arrange属性设置为QLLiveComponentArrangeHorizontal之后,就表示该Component是可以横向滑动的,DataSource会为该Component所在的索引处自动注册一个私有的UICollectionViewCell子类:QLOrthogonalScrollerEmbeddedCCell,并且这个cell中有一个已经添加的私有UICollectionView子类:QLOrthogonalScrollerEmbeddedScrollView为其子视图,对该Component注册的cell其实是给QLOrthogonalScrollerEmbeddedScrollView注册的,使用QLOrthogonalScrollerSectionController来为私有CollectionView实现数据源和代理方法,将对应的方法交给原始CollectionView的数据源和代理,也就是DataSource。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface QLOrthogonalScrollerSectionController: NSObject

@property (nonatomic, weak) UICollectionView *collectionView;
@property (nonatomic) QLOrthogonalScrollerEmbeddedScrollView *scrollView;
@property (nonatomic) NSInteger sectionIndex;

- (instancetype)initWithSectionIndex:(NSInteger)sectionIndex
collectionView:(UICollectionView *)collectionView
scrollView:(QLOrthogonalScrollerEmbeddedScrollView *)scrollView;

- (__kindof UICollectionViewCell *) dequeueReusableCell:(Class)cellClass
withReuseIdentifier:(NSString *)reuseIdentifier
atIndexPath:(NSIndexPath *)indexPath;
@end

性能优化

todo

最后

这个解耦完成之后,发现很多模块都可以使用它来完成,当然更多的是手里有锤子,看谁都是钉子的想法在作怪。然后就将本来叫做QLHomeModule、QLHomeDataSource、QLHomeComponent、QLHomeComponentLayout中的Home替换为了Live,并且抽离出来和首页无关的逻辑,虽然只是一个单词的替换,但更多的是表示这个组件可以使用在更多具有类似功能、样式的地方。

目前支持的功能比较有限,像是那种栅栏布局就不好实现,需要进行自定义UICollectionViewLayout,比如结合IBPCollectionViewCompositionalLayout或者系统的UICollectionViewCompositionalLayout来做一个拓展。另外在网络请求中去组装Component的操作,可以替换为使用后端下发的数据来完成,这个就需要结合具体的业务了,可以作为一个拓展方向。

参考文章