记一次筛选重构
前言
在我司产品中,有一个筛选模块的功能,由于历史原因,笔记和商品的筛选功
能,不能做到统一,而且扩展性极差,甚至影响到了UI交互的开发,在某个版本对其
进行了重构
本文主要和大家分享一下对筛选模块重构的一些经验,使大家能在后续相同功能开发中,
避免一些与我类似的错误设计
效果

正文
在我司的产品中,有如下一个筛选模块

主要的产品逻辑:
- 主面板支持价格输入
- 主面板支持单选(可反选)
- 主面板支持多选(可反选)
- 外露筛选可直接单选
- 外露筛选可展开单选模块
- 外露筛选可展开多选模块
- 有筛选时,外露的筛选按钮需要高亮
- 筛选项最多不超过15个
看到设计稿的第一眼,觉得左边主面板用一个UICollectionView就可以搞定,
但是再一想要同时支持单选和多选,显然一个UICollectionView搞不定,
那就用UITableView+UICollectionView吧,就是在UITableView的每个Cell上放一个UICollectionView。
当我吧啦吧啦把UI搭起来之后,发现单选的反选要手动支持。
UICollectionView默认是单选的,不支持反选,在设置allowsMultipleSelection为YES后,变为多选且可反选,
然后为了反选功能写下了下面一坨代码,

- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath中处理的逻辑是:
- 多选是否超过15的筛选项
- 单选时默认返回YES,因为单选返回NO,
UICollectionView点击不会生效了,只有在选中后再手动取消,如下

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath中处理的逻辑:
- 点击事件的回调
- 由于单选不能反选所以手动取消
坑一:
由于最初UI控件选择不当的原因(UITableView+UICollectionView),导致写了一
堆奇奇怪怪的代码出来,造成了后期代码极难维护
但无论怎么,到这里UI是搭建起来了(先run起来再说,优不优雅不重要),那现在就涉及到业务逻辑了。
其实主要业务逻辑就是把外露筛选、外露按钮、外露筛选展开、主筛选模块几个模块串起来即可,
按道理应该是这样一个结构:

但实际是这样的:

给大家看一部分接口:

其实看到主筛选面板的updateWithSelectedTagDictionarys方法,你就知道这是一段极难维护的代码
因为根本无法理解这是一个什么数据结构,而且这样的数据结构,在模块通信时,是极其痛苦的,他依赖的是具体,而不是抽象
如果是新人来维护这块代码,心里肯定是一万匹草泥马
坑二:
数据结构的设计不合理,导致功能模块难以维护和扩展
下面就进入重构后的代码
首先我抽象了一个类XYSFSelectedFilters,用来收集选中的筛选项,加工成后端需要
的数据结构,进行网络通信
其实整个筛选过程,就是收集数据,然后与后端交互
那么就完成了这个结构的第一步

XYSFSelectedFilters
@interface XYSFSelectedFilters : NSObject
@property (nonatomic, assign, readonly) NSInteger filterCount;
@property (nonatomic, copy, readonly) NSString *filterStr;
@property (nonatomic, assign, readonly) BOOL isOutOfRange;
@property (nonatomic, assign, readonly) BOOL isEmpty;
- (void)addFiltersFromFilterStr:(NSString *)filterStr;
- (void)mergeFilters:(XYSFSelectedFilters *)filters;
- (void)addFiltersToGroup:(NSString *)groupId
tagIds:(NSArray <NSString *> *)tagIds;
- (void)addFilterToGroup:(NSString *)groupId
tagId:(NSString *)tagId;
- (void)addSingleFilterToGroup:(NSString *)groupId
tagId:(NSString *)tagId;
- (void)removeFilterFromGroup:(NSString *)groupId
tagId:(NSString *)tagId;
- (void)removeFiltersWithGroupId:(NSString *)groupId;
- (void)removeAllFilters;
- (BOOL)containsFilter:(NSString *)filter;
- (BOOL)containsGroup:(NSString *)groupId;
- (NSOrderedSet <NSString *>*)objectForKeyedSubscript:(NSString *)key;
- (void)setObject:(NSOrderedSet <NSString *>*)object forKeyedSubscript:(NSString *)key;
@end
其中主要是对筛选项的增删操作,因为筛选的整个过程也就是选中和反选
,然后提供了filterStr的接口,用于与后端通信使用
@interface XYSFSelectedFilters()
@property (nonatomic, strong) NSMutableDictionary <NSString *, NSOrderedSet <NSString *> *> *selectedFilters;
@end
@implementation XYSFSelectedFilters
- (void)addFiltersFromFilterStr:(NSString *)filterStr {
if (NotEmptyValue(filterStr)) {
NSDictionary<NSString *,NSMutableOrderedSet *> *result = [XYSFSelectedFilters noteFiltersToDic:filterStr];
[self.selectedFilters addEntriesFromDictionary:result];
}
}
- (void)mergeFilters:(XYSFSelectedFilters *)filters {
for (NSString *key in filters.selectedFilters) {
NSMutableOrderedSet <NSString *> *selectedId = [self selectedTagIdWithType:key];
[selectedId unionOrderedSet:filters.selectedFilters[key]];
}
}
- (void)addFiltersToGroup:(NSString *)groupId tagIds:(NSArray<NSString *> *)tagIds {
NSMutableOrderedSet <NSString *> *selectedID = [self selectedTagIdWithType:groupId];
[selectedID addObjectsFromArray:tagIds];
}
- (void)addFilterToGroup:(NSString *)groupId tagId:(NSString *)tagId {
NSMutableOrderedSet <NSString *> *selectedID = [self selectedTagIdWithType:groupId];
[selectedID addObject:tagId];
}
- (void)addSingleFilterToGroup:(NSString *)groupId tagId:(NSString *)tagId {
NSMutableOrderedSet <NSString *> *selectedID = [self selectedTagIdWithType:groupId];
[selectedID removeAllObjects];
[selectedID addObject:tagId];
}
- (void)removeFilterFromGroup:(NSString *)groupId tagId:(NSString *)tagId {
NSMutableOrderedSet <NSString *> *selectedId = [self selectedTagIdWithType:groupId];
[selectedId removeObject:tagId];
if (selectedId.count < 1) {
self.selectedFilters[groupId] = nil;
}
}
- (void)removeFiltersWithGroupId:(NSString *)groupId {
self.selectedFilters[groupId] = nil;
}
- (void)removeAllFilters {
[self.selectedFilters removeAllObjects];
}
- (NSMutableOrderedSet <NSString *> *)selectedTagIdWithType:(NSString *)type {
NSMutableOrderedSet <NSString *> *selectedId = [self.selectedFilters[type] mutableCopy];
if (!selectedId) {
selectedId = NSMutableOrderedSet.new;
}
self.selectedFilters[type] = selectedId;
return selectedId;
}
- (NSString *)filterStr {
if (self.selectedFilters.count < 1) { return @""; }
NSMutableArray *reuslt = [NSMutableArray array];
for (NSString *key in self.selectedFilters) {
NSOrderedSet *set = self.selectedFilters[key];
if (!set || !key) { continue; }
NSArray *tags = set.array;
NSDictionary *dict = @{
@"type": key,
@"tags": tags
};
[reuslt addObject:dict];
}
return [NSJSONSerialization stringWithJSONObject:reuslt options:0 error:nil] ?: @"";
}
- (NSInteger)filterCount {
return self.allFilters.count;
}
- (NSOrderedSet <NSString *>*)allFilters {
NSMutableOrderedSet *result = NSMutableOrderedSet.new;
for (NSOrderedSet *set in self.selectedFilters.allValues) {
[result unionOrderedSet:set];
}
return result.copy;
}
- (BOOL)containsFilter:(NSString *)filter {
return [self.allFilters containsObject:filter];
}
- (BOOL)containsGroup:(NSString *)groupId {
return self.selectedFilters[groupId].count > 0;
}
- (NSMutableDictionary<NSString *, NSOrderedSet<NSString *> *> *)selectedFilters {
if (!_selectedFilters) {
_selectedFilters = NSMutableDictionary.dictionary;
}
return _selectedFilters;
}
+ (NSDictionary<NSString *,NSMutableOrderedSet *> *)noteFiltersToDic:(NSString *)filterStr {
NSParameterAssert(NotEmptyValue(filterStr));
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSArray *filtersArray = [NSJSONSerialization JSONObjectWithString:filterStr options:NSJSONReadingAllowFragments error:nil];
if (!filtersArray) {
return result;
}
for (NSDictionary *obj in filtersArray) {
if ([obj isKindOfClass:[NSDictionary class]]) {
if ([obj[@"tags"] isKindOfClass:[NSArray class]]) {
NSMutableOrderedSet *tags = [NSMutableOrderedSet orderedSetWithArray:obj[@"tags"]];
result[obj[@"type"]] = tags;
}
}
}
return result;
}
- (NSOrderedSet <NSString *>*)objectForKeyedSubscript:(NSString *)key {
return self.selectedFilters[key];
}
- (void)setObject:(NSOrderedSet <NSString *>*)object forKeyedSubscript:(NSString *)key {
self.selectedFilters[key] = object;
}
- (NSString *)description {
NSMutableString *result = [NSMutableString stringWithString:@"{\n"];
for (NSString *key in self.selectedFilters) {
[result appendFormat:@"%@: %@,\n", key, self.selectedFilters[key]];
}
[result appendString:@"\n}"];
return result.copy;
}
- (BOOL)isOutOfRange {
if (self.filterCount > 14) {
[[XYAlertCenter createTextItemWithTextOnTop:@"最多只能选15个哦"] show];
return YES;
}
return NO;
}
- (BOOL)isEmpty {
return self.filterCount < 1;
}
@end
内部数据结构用的是NSMutableDictionary去存储NSOrderedSet,
用NSMutableDictionary,我觉得大家都很容易理解,
但是为什么用NSOrderedSet,估计有人会有疑问,为什么不用NSSet,或者NSArray
首先在这里NSSet是天生适合这个业务场景的,筛选项肯定不需要重复,
其次在做containsObject判断时,NSSet是O(1)的操作,NSArray是O(n)
理论上,筛选也不需要有序啊,但是这个业务场景中,有个价格输入的筛选项
,需要客户端把价格顺序弄好,传给后端,因为在用户只输入了最低价,不输入最高价
或者只输入最高价,没有最低价时后端是没法判断的(这里的具体实现,可看Demo中的代
码),所以选择了NSOrderedSet
XYPHSFViewControllerPresenter
接下来我又定义了XYPHSFViewControllerPresenter
@protocol XYPHSFViewControllerPresenter <NSObject>
@property (nonatomic, strong, readonly) XYSFSelectedFilters *selectedFilters;
@optional
- (void)referenceSelectedFilters:(XYSFSelectedFilters *)selectedFilters;
@end
它的主要作用是,让View层,拿到XYSFSelectedFilters,View层就自己处理自己的增
删操作,就不需要回到VC中去做了
XYPHSFViewControllerDelegate
@protocol XYPHSFViewControllerDelegate <NSObject>
- (void)searchFilterViewControllerDoneButtonClicked:(UIViewController <XYPHSFViewControllerPresenter> *)viewController;
- (void)searchFilterViewControllerDidChangedSelectedTag:(UIViewController <XYPHSFViewControllerPresenter> *)viewController;
@end
这个协议的主要作用是:当筛选变化,或者筛选完成时,回调给网络层,做相应变化
通过以上两个协议,就将几个筛选模块链接了起来
再看看重构后的模块接口
主筛选模块

外露排序view

外露筛选view

可以看到几个模块已经没有了数据交互的接口,那他们的数据怎么通信的呢?
在主筛选面板中,有一个- (void)referenceSelectedFilters:(XYSFSelectedFilters *)selectedFilters接口,他的作用就是强引用FilterRefactorDataSourcenew出来的,XYSFSelectedFilters *selectedFilters,这里利用强引用的特性,就可以改变源数据了
两个View也是同理,通过响应链拿到VC,在通过抽象出来的协议,就可以对源数据操作了

在这里整个重构思路就介绍完了,没有介绍清楚的地方,可以看Demo里面的源码
总结
做完这次重构,想起了高中数学老师的一句话:计算方法决定计算过程,
就像我们高中做立体几何,如果用立体坐标系去做,会写一堆复杂的过程,
但是用做垂线的方法,过程却极其简单,但是想要找到那条垂线,却又是一个很难的问题。
在我们真实开发过程中,经常不能一下想到最好的设计方案,这是很正常的,先让功能
跑起来再说,也不必在找出最优解上面耗费太多时间,那样只会拖慢开发进度,只要后
期我们多思考,多去琢磨那些不满意的地方,肯定能做出我们心里满意的设计。
还有重构一定要加开关,关键时刻可救我们一命。