在我司产品中,有一个筛选模块的功能,因为历史缘由,笔记和商品的筛选功git
能,不能作到统一,并且扩展性极差,甚至影响到了UI交互的开发,在某个版本对其github
进行了重构后端
本文主要和你们分享一下对筛选模块重构的一些经验,使你们能在后续相同功能开发中,网络
避免一些与我相似的错误设计数据结构
在我司的产品中,有以下一个筛选模块app
主要的产品逻辑:ui
看到设计稿的第一眼,以为左边主面板用一个UICollectionView
就能够搞定,atom
可是再一想要同时支持单选和多选,显然一个UICollectionView
搞不定,spa
那就用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
,用来收集选中的筛选项,加工成后端须要
的数据结构,进行网络通讯
其实整个筛选过程,就是收集数据,而后与后端交互
那么就完成了这个结构的第一步
@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
@protocol XYPHSFViewControllerPresenter <NSObject>
@property (nonatomic, strong, readonly) XYSFSelectedFilters *selectedFilters;
@optional
- (void)referenceSelectedFilters:(XYSFSelectedFilters *)selectedFilters;
@end
复制代码
它的主要做用是,让View层,拿到XYSFSelectedFilters
,View层就本身处理本身的增
删操做,就不须要回到VC中去作了
@protocol XYPHSFViewControllerDelegate <NSObject>
- (void)searchFilterViewControllerDoneButtonClicked:(UIViewController <XYPHSFViewControllerPresenter> *)viewController;
- (void)searchFilterViewControllerDidChangedSelectedTag:(UIViewController <XYPHSFViewControllerPresenter> *)viewController;
@end
复制代码
这个协议的主要做用是:当筛选变化,或者筛选完成时,回调给网络层,作相应变化
经过以上两个协议,就将几个筛选模块连接了起来
能够看到几个模块已经没有了数据交互的接口,那他们的数据怎么通讯的呢?
在主筛选面板中,有一个- (void)referenceSelectedFilters:(XYSFSelectedFilters *)selectedFilters
接口,他的做用就是强引用FilterRefactorDataSource
new出来的,XYSFSelectedFilters *selectedFilters
,这里利用强引用的特性,就能够改变源数据了
两个View也是同理,经过响应链拿到VC,在经过抽象出来的协议,就能够对源数据操做了
在这里整个重构思路就介绍完了,没有介绍清楚的地方,能够看Demo里面的源码
作完此次重构,想起了高中数学老师的一句话:计算方法决定计算过程,
就像咱们高中作立体几何,若是用立体坐标系去作,会写一堆复杂的过程,
可是用作垂线的方法,过程却极其简单,可是想要找到那条垂线,却又是一个很难的问题。
在咱们真实开发过程当中,常常不能一下想到最好的设计方案,这是很正常的,先让功能
跑起来再说,也没必要在找出最优解上面耗费太多时间,那样只会拖慢开发进度,只要后
期咱们多思考,多去琢磨那些不满意的地方,确定能作出咱们内心满意的设计。
还有重构必定要加开关,关键时刻可救咱们一命。