内容简介:在我司产品中,有一个筛选模块的功能,由于历史原因,能,不能做到统一,而且扩展性极差,甚至影响到了UI交互的开发,在某个版本对其进行了重构
在我司产品中,有一个筛选模块的功能,由于历史原因, 笔记 和 商品 的筛选功
能,不能做到统一,而且扩展性极差,甚至影响到了UI交互的开发,在某个版本对其
进行了重构
本文主要和大家分享一下对筛选模块重构的一些经验,使大家能在后续相同功能开发中,
避免一些与我类似的错误设计
效果
正文
在我司的产品中,有如下一个筛选模块
主要的产品逻辑:
- 主面板支持价格输入
- 主面板支持单选(可反选)
- 主面板支持多选(可反选)
- 外露筛选可直接单选
- 外露筛选可展开单选模块
- 外露筛选可展开多选模块
- 有筛选时,外露的筛选按钮需要高亮
- 筛选项最多不超过15个
看到设计稿的第一眼,觉得左边主面板用一个 UICollectionView
就可以搞定,
但是再一想要同时支持 单选 和 多选 ,显然一个 UICollectionView
搞不定,
那就用 UITableView
+ UICollectionView
吧,就是在 UITableView
的每个Cell上放一个 UICollectionView
。
当我吧啦吧啦把UI搭起来之后,发现单选的反选要手动支持。
UICollectionView
默认是单选的,不支持反选,在设置 allowsMultipleSelection
为YES后,变为多选且可反选,
然后为了反选功能写下了下面一坨代码,
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
中处理的逻辑是:
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
接口,他的作用就是强引用 FilterRefactorDataSource
new出来的, XYSFSelectedFilters *selectedFilters
,这里利用强引用的特性,就可以改变源数据了
两个View也是同理,通过响应链拿到VC,在通过抽象出来的协议,就可以对源数据操作了
在这里整个重构思路就介绍完了,没有介绍清楚的地方,可以看Demo里面的源码
总结
做完这次重构,想起了高中数学老师的一句话: 计算方法决定计算过程 ,
就像我们高中做立体几何,如果用立体坐标系去做,会写一堆复杂的过程,
但是用做垂线的方法,过程却极其简单,但是想要找到那条垂线,却又是一个很难的问题。
在我们真实开发过程中,经常不能一下想到最好的设计方案,这是很正常的,先让功能
跑起来再说,也不必在找出最优解上面耗费太多时间,那样只会拖慢开发进度,只要后
期我们多思考,多去琢磨那些不满意的地方,肯定能做出我们心里满意的设计。
还要重构一定要加开关,关键时刻可救我们一命。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- JS中的数组过滤,从简单筛选到多条件筛选
- python素数筛选法浅析
- python如何在列表、字典中筛选数据
- iOS – tableView类型的筛选框实现
- 深度学习在封面图筛选中的应用
- python使用筛选法计算小于给定数字的所有素数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。