在我司產品中,有一個篩選模塊的功能,因爲歷史緣由,筆記和商品的篩選功git
能,不能作到統一,並且擴展性極差,甚至影響到了UI交互的開發,在某個版本對其github
進行了重構後端
本文主要和你們分享一下對篩選模塊重構的一些經驗,使你們能在後續相同功能開發中,markdown
避免一些與我相似的錯誤設計網絡
在我司的產品中,有以下一個篩選模塊數據結構
主要的產品邏輯:app
看到設計稿的第一眼,以爲左邊主面板用一個UICollectionView
就能夠搞定,oop
可是再一想要同時支持單選和多選,顯然一個UICollectionView
搞不定,atom
那就用UITableView
+UICollectionView
吧,就是在UITableView
的每一個Cell上放一個UICollectionView
。spa
當我吧啦吧啦把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裏面的源碼
作完此次重構,想起了高中數學老師的一句話:計算方法決定計算過程,
就像咱們高中作立體幾何,若是用立體座標系去作,會寫一堆複雜的過程,
可是用作垂線的方法,過程卻極其簡單,可是想要找到那條垂線,卻又是一個很難的問題。
在咱們真實開發過程當中,常常不能一下想到最好的設計方案,這是很正常的,先讓功能
跑起來再說,也沒必要在找出最優解上面耗費太多時間,那樣只會拖慢開發進度,只要後
期咱們多思考,多去琢磨那些不滿意的地方,確定能作出咱們內心滿意的設計。
還有重構必定要加開關,關鍵時刻可救咱們一命。