记一次筛选重构

记一次筛选重构

前言

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

能,不能作到统一,并且扩展性极差,甚至影响到了UI交互的开发,在某个版本对其github

进行了重构后端

本文主要和你们分享一下对筛选模块重构的一些经验,使你们能在后续相同功能开发中,网络

避免一些与我相似的错误设计数据结构

效果

正文

在我司的产品中,有以下一个筛选模块app

主要的产品逻辑:ui

  • 主面板支持价格输入
  • 主面板支持单选(可反选)
  • 主面板支持多选(可反选)
  • 外露筛选可直接单选
  • 外露筛选可展开单选模块
  • 外露筛选可展开多选模块
  • 有筛选时,外露的筛选按钮须要高亮
  • 筛选项最多不超过15个

看到设计稿的第一眼,以为左边主面板用一个UICollectionView就能够搞定,atom

可是再一想要同时支持单选多选,显然一个UICollectionView搞不定,spa

那就用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里面的源码

总结

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

就像咱们高中作立体几何,若是用立体坐标系去作,会写一堆复杂的过程,

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

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

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

期咱们多思考,多去琢磨那些不满意的地方,确定能作出咱们内心满意的设计。

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

Demo

相关文章
相关标签/搜索