记一次筛选重构

在我司产品中,有一个筛选模块的功能,由于历史原因, 笔记商品 的筛选功

能,不能做到统一,而且扩展性极差,甚至影响到了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里面的源码

总结

做完这次重构,想起了高中数学老师的一句话: 计算方法决定计算过程

就像我们高中做立体几何,如果用立体坐标系去做,会写一堆复杂的过程,

但是用做垂线的方法,过程却极其简单,但是想要找到那条垂线,却又是一个很难的问题。

在我们真实开发过程中,经常不能一下想到最好的设计方案,这是很正常的,先让功能

跑起来再说,也不必在找出最优解上面耗费太多时间,那样只会拖慢开发进度,只要后

期我们多思考,多去琢磨那些不满意的地方,肯定能做出我们心里满意的设计。

还要重构一定要加开关,关键时刻可救我们一命。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章