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,UICollectionViewClicking 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.

Demo