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