项目代码重构-继承结构调整

Posted by Maqy on June 30, 2018

背景:去年十月入职,针对项目中存在的问题提了一系列的重构方案,到现在大概完成了百分之三四十样子,后面会针对各个问题整理一系列的笔记。首先目前的业务线是组件开发的模式,其实主站的基础组件封装的还是不错的,但目前的业务线的代码却有些问题。

问题:例如说,我们的主要model是XXXModel,该model会在各个列表中出现,feed流,个人页,推荐流,主题信息流等,以及详情页。那么现在采取的实现方式是,feed流继承baseTableVeiwVC,而其他的列表页继承自feed流(为了重用卡片的相关逻辑),然后通过重写各个小的控制方法来实现功能。类似这种VC的卡片是通过ComponentKit(下面简称CK)来实现的,详情页通过nativeView来实现。也就是维护了两套view的代码。

重构重点

  1. 去除不相关的VC的继承关系,从而去掉子VC中的各种重写方法,这样太不利于理解了。
  2. 去除详情页的nativeView代码,用一套就可以了

重构开始

抽取卡片handler类

因为该类型的卡片被各列表VC重用,所以其事件处理也肯定会被各个VC重用,那么要改变类的继承结构就首先要抽出该事件处理类,但是VC可能存在不同section的情况,所以我们抽的handler要能够用在多个section的VC中。

@protocol XXXCellHandlerDelegate <NSObject>

/// 所在VC的navigationController
- (UINavigationController *)handlerNavigationController;
/// model所在的section
- (NSUInteger)handler:(XXXCellHandler *)handler sectionForModel:(id)model;
/// model所在的viewModel
- (ListViewModel *)handler:(XXXCellHandler *)handler viewModelForModel:(id)model;

@end

@interface XXXCellHandler : NSObject

@property (nonatomic, weak) id<XXXCellHandlerDelegate> delegate;
/// 是否应该做某事
@property (nonatomic, assign) BOOL shouldDoSomeThing;
...

@end
  1. 该类通过delegate获取所在VC,viewModel,section等信息,来做进一步处理。
  2. 如果某个事件在不同VC中有分叉行为,那么建议通过property来控制,而不是通过继承来实现,不然不利于维护。当然,如果分叉行为很复杂,还是通过继承来实现比较好。
- (void)mapModel:(id)model withResult:(void(^)(ListViewModel *viewModel, NSUInteger section))resultBlock {
    ListViewModel *viewModel = nil;
    NSUInteger section = NSNotFound;
    
    if ([self.delegate respondsToSelector:@selector(handler:viewModelForModel:)]) {
        viewModel = [self.delegate handler:self viewModelForModel:model];
    }
    if ([self.delegate respondsToSelector:@selector(handler:sectionForModel:)]) {
        section = [self.delegate handler:self sectionForModel:model];
    }
    
    resultBlock(viewModel, section);
}
  1. 通过map方法来映射点击事件的model所对应的section和viewModel,来支持多个section的VC

抽取上边的handler后,如果一个VC要构造新的列表那么只需要接入handler,然后实现delegate就可以完成了。

抽取卡片相关的其他功能

和handler类似,卡片相关的功能还有其他,例如视频自动播放,各个VC都会存在的,那么这时候抽取一个manager就很有必要了

更改VC继承结构

首先从子类开始,因为feedVC太庞大了,不能动,所以只能从比较小的子VC开始。

重构前的结构大概是这样子的:

	BaseTableVC (tableView的基础VC,包含上拉下拉加载等逻辑)
	|
	FeedVC(这个就比较大了,包含卡片相关的所有逻辑,包含打点,视频相关,已读逻辑等等等)
	|			|			|
	OtherVC1	otherVC2	otherVC3	...
	
	这种结构决定了几个比较大的缺陷:
	1.FeedVC要包含绝大多数逻辑
	2.FeedVC要分出很多小的方法,来进行逻辑判断,而每个子VC都要都要重写很多方法,为了去掉不想要的逻辑会有很多空方法
	3.FeedVC会很大
	4.扩展受限,FeedVC添加功能,还要改子类
	5.一无是处...

重构后,VC只需要持有handler,并实现其delegate就可以,大致结构是这样:

cellHandler(用来处理卡片的事件,VC持有并实现delegate就可以,支持多section的tableVC)
videoManager(自动播放的管理类,VC持有并实现delegate就可接入)

VC的结构就可以调整成不相互影响的结构了:
	BaseTableVC (tableView的基础VC,包含上拉下拉加载等逻辑)
	|			|			|			|
	FeedVC		OtherVC1	otherVC2	otherVC3	...
	这样调整的好处:
	1.VC间互不影响
	2.方便接入卡片的相关逻辑,想接哪个就引入就可以了
	3.方便控制逻辑,通过property去控制业务走向
	4.没有了那么多不相关的逻辑,主线清晰明了

重构详情页

调整前结构:

首先我们的卡片逻辑很多,结合起来比较复杂,要实现还是比较麻烦的。

而普通的卡片列表使用CK来实现的,也就是我们有一套CK的view。
但详情页使用native实现的,也就是我们有两套view,但表现是一致的。

normalTableView ------> ComponentKit‘s card
detailVC		------> native's card

那么结果就是维护麻烦,工程量翻倍,什么东西都是两份(delegate, event, view ... )。

调整后的结构:

把详情页看成一个tableVC

BaseTableVC (tableView的基础VC,包含上拉下拉加载等逻辑)
|			|			|			|			|
detailVC	FeedVC		OtherVC1	otherVC2	otherVC3	...

一样的配方,不一样的味道,世界都清净了,完美套用 

总结

通过这部分代码的重构总结了一些编码上的规范:

  1. 明确各角色的功能,尤其是重用度较高的时候,不要把所有功能都写在VC里,不要采取继承+重写的方式来实现,这样真的很难读懂,因为没有明确暴漏业务上的逻辑。
  2. 组合优于继承,继承用的过多会造成类过多,更不利于维护,所以当我们实现功能的时候要优先考虑组合。
  3. 明确各个角色的类型,例如viewModel负责object的维护和网络加载等,VC更注重于角色间的协调及事件处理,那么如果卡片复用度较高,则可以抽取和卡片对应的handler,来处理卡片的事件,方便服用。