記一次篩選重構

記一次篩選重構

前言

在我司產品中,有一個篩選模塊的功能,因爲歷史緣由,筆記商品的篩選功git

能,不能作到統一,並且擴展性極差,甚至影響到了UI交互的開發,在某個版本對其github

進行了重構後端

本文主要和你們分享一下對篩選模塊重構的一些經驗,使你們能在後續相同功能開發中,markdown

避免一些與我相似的錯誤設計網絡

效果

正文

在我司的產品中,有以下一個篩選模塊數據結構

主要的產品邏輯:app

  • 主面板支持價格輸入
  • 主面板支持單選(可反選)
  • 主面板支持多選(可反選)
  • 外露篩選可直接單選
  • 外露篩選可展開單選模塊
  • 外露篩選可展開多選模塊
  • 有篩選時,外露的篩選按鈕須要高亮
  • 篩選項最多不超過15個

看到設計稿的第一眼,以爲左邊主面板用一個UICollectionView就能夠搞定,oop

可是再一想要同時支持單選多選,顯然一個UICollectionView搞不定,atom

那就用UITableView+UICollectionView吧,就是在UITableView的每一個Cell上放一個UICollectionViewspa

當我吧啦吧啦把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

相關文章
相關標籤/搜索