Remember a filter refactoring
preface
In our products, there is a screening module function, due to historical reasons, notes and commodity screening work
Can, can not be unified, and the scalability is very poor, and even affected the development of UI interaction, in a version of its
Refactoring
This article is to share with you some experience on the filter module reconstruction, so that you can in the subsequent development of the same function,
Avoid design mistakes like mine
The effect
The body of the
In our products, there is the following screening module
Main product logic:
- The main panel supports price input
- The main panel supports radio selection (reversible)
- The main panel supports multiple selection (reversible)
- Exposure screening can be selected directly
- Exposed filter expandable radio module
- Exposed filter can expand multiple selection modules
- The exposed filter button needs to be highlighted when there is a filter
- The number of filters cannot exceed 15
At first glance, I thought I could just use a UICollectionView for the left main panel,
But if you want to support both single and multiple options, obviously one UICollectionView doesn’t work,
So let’s do UITableView+UICollectionView, which is putting a UICollectionView on each Cell of a UITableView.
When I put the UI together, I realized that I had to manually support the reverse selection of radio selections.
By default, UICollectionView is single and does not support unselection. After setting allowsMultipleSelection to YES, UICollectionView becomes multiple and unselected.
Then I wrote the following piece of code for the reverse selection function,
– (BOOL) collectionView: (UICollectionView *) collectionView shouldSelectItemAtIndexPath: (NSIndexPath *) indexPath processing logic is:
- Multiple specifies whether the value exceeds 15
- Radio returns YES by default, because radio returns NO,
UICollectionView
Clicking on it will not take effect, only manually cancel it after it is selected, as shown below
– (void) collectionView: (UICollectionView *) collectionView didSelectItemAtIndexPath: (indexPath NSIndexPath *) in treatment of logic:
- Click the event callback
- The radio selection cannot be reversed, so manually cancel it
Pit one:
Due to the improper selection of UI controls (UITableView+UICollectionView) at the beginning of the process, a
A bunch of weird code comes out, making the later code extremely difficult to maintain
But at this point, the UI is built (run first, elegance doesn’t matter), and now the business logic is involved.
In fact, the main business logic is the exposed screening, exposed button, exposed screening expansion, the main screening module can be strung together several modules,
It should look like this:
But here’s what happened:
I’ll show you some of the interfaces:
Actually see the main screen panel updateWithSelectedTagDictionarys method, you will know that this is a very difficult to maintain the code
Because it’s impossible to understand what a data structure is, and such a data structure, when it comes to module communication, is extremely painful, it relies on the concrete, not the abstract, right
If a new person is trying to maintain this piece of code, he or she is going to have ten thousand horses
Pit 2:
The unreasonable design of data structure makes it difficult to maintain and extend functional modules
Here’s the refactored code
First, I abstract a class called XYSFSelectedFilters that collects selected filters and processes them into back-end needs
Data structure for network communication
The whole filtering process is about collecting data and interacting with the back end
So that completes the first step of the structure
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
Copy the code
It is mainly the operation of adding and deleting filter items, because the whole process of filtering is also selected and unselected
And then provides an interface for filterStr to communicate with the back end
@interface XYSFSelectedFilters(a)
@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 = [selfselectedTagIdWithType: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:@" You can only choose 15."] show];
return YES;
}
return NO;
}
- (BOOL)isEmpty {
return self.filterCount < 1;
}
@end
Copy the code
The internal data structure uses an NSMutableDictionary to store the NSOrderedSet,
With NSMutableDictionary, I think it’s pretty easy for everyone to understand,
But why do we use NSOrderedSet, and you might wonder, why don’t we use NSSet, or NSArray
First of all, NSSet is a natural fit for this business scenario, the filters definitely don’t need to be repeated,
Second of all, when I do containsObject, NSSet is O(1), NSArray is O(n).
In theory, filters don’t need to be ordered, but in this business scenario, there is a filter for price input
The client needs to get the price sequence right and pass it to the backend, because the user only input the lowest price, not the highest price
Or only input the highest price, there is no minimum price when the back end is not able to determine (see the specific implementation here, the Demo generation
Code), so NSOrderedSet is selected
XYPHSFViewControllerPresenter
I then defines XYPHSFViewControllerPresenter again
@protocol XYPHSFViewControllerPresenter <NSObject>
@property (nonatomic.strong.readonly) XYSFSelectedFilters *selectedFilters;
@optional
- (void)referenceSelectedFilters:(XYSFSelectedFilters *)selectedFilters;
@end
Copy the code
And the main thing it does is it lets the View layer take the XYSFSelectedFilters, and the View layer processes its own increment
Delete, you don’t have to go back to VC to do it
XYPHSFViewControllerDelegate
@protocol XYPHSFViewControllerDelegate <NSObject>
- (void)searchFilterViewControllerDoneButtonClicked:(UIViewController <XYPHSFViewControllerPresenter> *)viewController;
- (void)searchFilterViewControllerDidChangedSelectedTag:(UIViewController <XYPHSFViewControllerPresenter> *)viewController;
@end
Copy the code
The main purpose of this protocol is to call back to the network layer when the filtering changes or completes
With the above two protocols, several filter modules are linked together
Take a look at the refactored module interface
Main filter module
Expose sort view
Expose filter view
You can see that several modules have no data interface. How do their data communicate?
In the main screen panel, there is a – (void) referenceSelectedFilters: (XYSFSelectedFilters *) selectedFilters interface, His role is strong reference FilterRefactorDataSourcenew, XYSFSelectedFilters * selectedFilters, using the characteristics of strong references here can change the source data
The same is true for the two views, which get to the VC through the response chain, and then operate on the source data through the abstract protocol
Here the whole refactoring idea is finished, there is no clear place to introduce, you can see the Demo inside the source code
conclusion
After this reconstruction, I remembered the words of my high school math teacher: The calculation method determines the calculation process.
Just like when we did solid geometry in high school, if we did it in solid coordinates, it would be a bunch of complicated procedures,
But with the vertical line, the process is extremely simple, but finding that vertical line is a very difficult problem.
In our real development process, we often can’t come up with the best design solution at once, which is normal, let the function first
At the same time, you don’t have to spend too much time trying to find the optimal solution, which will only slow down the development process, as long as the
We think more, more to think about those dissatisfied places, we can certainly make our hearts satisfied with the design.
And refactoring must be on the switch, the critical moment can save our lives.