内容简介:在我司产品中,有一个筛选模块的功能,由于历史原因,能,不能做到统一,而且扩展性极差,甚至影响到了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使用筛选法计算小于给定数字的所有素数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
.NET本质论 第1卷:公共语言运行库
博克斯 (BoxDon) / 张晓坤 / 中国电力出版社 / 2004-1 / 48.00元
本书由10章组成,探讨了CLR即公共语言运行库,涵盖了基本类型、实例、方法调用和消息、AppDomain、安全、以及CLR外部世界。一起来看看 《.NET本质论 第1卷:公共语言运行库》 这本书的介绍吧!