[[153315]] Opening Today, we will start with a small function and implement it without thinking. Product Repository: Filtering Operation Code start There is a product library, we want to filter it. The first requirement is not complicated. Requirement 1: Find all products in the warehouse that are red First Attempt: Hard Code Let's first implement it in the simplest way, hard coding - - (NSArray *)findAllRedProducts:(NSArray *)products
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.color == RED) {
- [list addObject:product];
- }
- }
- return list;
- }
If the world were eternally static, this would be understandable, but the world is often not like this. Then, the second requirement came Requirement 2: Find all green products in the warehouse Second Attempt: Parameterizing Copy-Paste is the most common mistake made by most programmers, which introduces a lot of duplicate code. - - (NSArray *)findAllGreenProducts:(NSArray *)products
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.color == GREEN) {
- [list addObject:product];
- }
- }
- return list;
- }
In order to eliminate hard coding and obtain reusable code, simple parameterized design can be introduced. - - (NSArray *)findProducts:(NSArray *)products byColor:(ProductColor)color
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.color == color) {
- [list addObject:product];
- }
- }
- return list;
- }
Finally, I can rest assured. How can our product manager make you feel comfortable at this time? Requirement 3 is here again. Requirement 3: Find all products whose weight is less than 10 Third Attempt: Parameterizing with Every Attribute You Can Think Of Most programmers still use Copy-Paste to solve this problem. To avoid the bad habit of Copy-Paste, the most effective feedback is to disable this shortcut key, so that you can remind yourself to make better designs every time you try Copy-Paste. - - (NSArray *)findProducts:(NSArray *)products byWeith:( float )weight
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (product.weight < weight) {
- [list addObject:product];
- }
- }
- return list;
- }
In order to eliminate duplicate code between the two, simple parameterization often cannot completely solve such problems. On the contrary, it will introduce excessive complexity and incidental costs. - - (NSArray *)findProducts:(NSArray *)products byColor:(ProductColor)color byWeith:( float )weight type:( int )type
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if ((type == 1 ) && product.color == color) {
- [list addObject:product];
- continue ;
- }
- else if ((type == 2 ) && (product.weight < weight))
- {
- [list addObject:product];
- continue ;
- }
- }
- return list;
- }
In daily work, this implementation method is very common. The parameter list of the function continues to increase as the demand increases, the function logic assumes more and more responsibilities, and the logic becomes more and more difficult to control. Designs that respond to changes through parameter configuration are often failed designs It is easy to lead to complex logic control, causing additional accidental complexity Forth Attempt: Abstracting over Criteria To do this, abstraction is needed so that the traversal algorithm and search criteria can change independently without affecting each other. - @interface ProductSpec : NSObject
- - (BOOL)satisfy:(Product *)product;
- @end
At this point, the filter's algorithm logic is closed. Of course, the function name needs to be renamed to make its algorithm implementation more universal. - - (NSArray *)findProducts:(NSArray *)products bySpec:(ProductSpec *)spec
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if ([spec satisfy:product]) {
- [list addObject:product];
- }
- }
- return list;
- }
Encapsulate various changes through reusable classes to keep the factors of change within the smallest range. - @interface ColorSpec()
- @property (nonatomic, assign) ProductColor color;
- @end
- @implementation ColorSpec
- + (instancetype)specWithColor:(ProductColor)color
- {
- ColorSpec *spec = [[ColorSpec alloc] init];
- spec.color = color;
- return spec;
- }
- - (BOOL)satisfy:(Product *)product
- {
- return product.color == RED;
- }
- @end
- @interface BelowWeightSpec()
- @property (nonatomic, assign) float limit;
- @end
- @implementation BelowWeightSpec
- + (instancetype)specWithBelowWeight:( float )limit
- {
- BelowWeightSpec *spec = [[BelowWeightSpec alloc] init];
- spec.limit = limit;
- return spec;
- }
- - (BOOL)satisfy:(Product *)product
- {
- return (product.weight < _limit);
- }
- @end
The user interface has also become much simpler and more expressive. - [self findProducts:_products bySpec:[ColorSpec specWithColor:RED]];
This is a classic OO design. Readers who are familiar with design patterns are already accustomed to it. Design patterns are good things, but they are often abused. For this reason, we cannot copy them rigidly, but introduce design patterns to get a simpler design. This process is very natural. I talked to the masters and asked them why the design pattern was introduced here. The answer I got was: intuition. Forget all the design patterns. It doesn’t matter if it is a pattern or not. If the design is simple, it is a pattern. There is also an obvious bad smell. Both ColorSpec and BelowWeightSpec need to inherit ProductSpec, both need to define a constructor and a private field, and override the satisfy method, which are full of repeated structures. Do you think the current writing method is sufficient? Don't worry, let's take a look at the next requirement Requirement 4: Find all products whose color is red and whose weight is less than 10 - Firth Attempt: Composite Criteria
-
- According to the existing code structure, it is often easy to design an implementation similar to ColorAndBelowWeightSpec.
- @interface ColorAndBelowWeigthSpec()
- @property (nonatomic, assign) ProductColor color;
- @property (nonatomic, assign) float limit;
- @end
- @implementation ColorAndBelowWeigthSpec
- + (instancetype)specWithColor:(ProductColor)color beloWeigth:( float )limit
- {
- ColorAndBelowWeigthSpec *spec = [[ColorAndBelowWeigthSpec alloc] init];
- spec.color = color;
- spec.limit = limit;
- return spec;
- }
- - (BOOL)satisfy:(Product *)product
- {
- return product.color == _color || (product.weight < _limit);
- }
- @end
There are two obvious bad smells: Naming containing "and" is often a signal that violates the single responsibility principle. There is an obvious duplication between the implementation of ColorAndBelowWeightSpec and ColorSpec,BelowWeightSpec At this point, we need to find a more essential abstraction to express the design, and/or/not semantics can best solve this type of problem. Composite Spec: AndSpec, OrSpec, NotSpec Atomic Spec: ColorSpec, BeblowWeightSpec - @interface AndSpec()
- @property (nonatomic, strong) NSArray *specs;
- @end
- @implementation AndSpec
- + (instancetype)spec:(ProductSpec *)spec, ... NS_REQUIRES_NIL_TERMINATION
- {
- va_list args;
- va_start( args, spec );
- NSMutableArray *mArray = [@[spec] mutableCopy];
- for ( ;; )
- {
- id tempSpec = va_arg( args, id );
- if (tempSpec == nil)
- break ;
- [mArray addObject:tempSpec];
- }
- va_end( args );
- AndSpec *andSpec = [[AndSpec alloc] init];
- andSpec.specs = [mArray copy];
- return andSpec;
- }
- - (BOOL)satisfy:(Product *)product
- {
- for (ProductSpec *spec in _specs) {
- if (![spec satisfy:product]) {
- return NO;
- }
- }
- return YES;
- }
- @end
- @interface OrSpec ()
- @property (nonatomic, strong) NSArray *specs;
- @end
- @implementation OrSpec
- + (instancetype)spec:(ProductSpec *)spec, ... NS_REQUIRES_NIL_TERMINATION
- {
- va_list args;
- va_start( args, spec );
- NSMutableArray *mArray = [@[spec] mutableCopy];
- for ( ;; )
- {
- id tempSpec = va_arg( args, id );
- if (tempSpec == nil)
- break ;
- [mArray addObject:tempSpec];
- }
- va_end( args );
- OrSpec *orSpec = [[OrSpec alloc] init];
- orSpec.specs = [mArray copy];
- return orSpec;
- }
- - (BOOL)satisfy:(Product *)product
- {
- for (ProductSpec *spec in _specs) {
- if ([spec satisfy:product]) {
- return YES;
- }
- }
- return NO;
- }
- @end
- @interface NotSpec ()
- @property (nonatomic, strong) ProductSpec *spec;
- @end
- @implementation NotSpec
- + (instancetype)spec:(ProductSpec *)spec
- {
- NotSpec *notSpec = [[NotSpec alloc] init];
- notSpec.spec = spec;
- return notSpec;
- }
- - (BOOL)satisfy:(Product *)product
- {
- if (![_spec satisfy:product]) {
- return YES;
- }
- return NO;
- }
- @end
The requirement can be met by combining ColorSpec and BelowWeightSpec through AndSpec, which is simple, beautiful and expressive. - [self findProducts:_products bySpec:[AndSpec spec:[ColorSpec specWithColor:RED], [BelowWeightSpec specWithBelowWeight: 10 ], nil]];
But there are two serious drawbacks to this design: There is obvious code duplication between AndSpec and OrSpec. The first intuition of OO design is to eliminate duplication by extracting base classes. - @interface CombinableSpec ()
- @property (nonatomic, strong) NSArray *specs;
- @end
- @implementation CombinableSpec
- + (instancetype)spec:(CombinableSpec *)spec, ... NS_REQUIRES_NIL_TERMINATION
- {
- va_list args;
- va_start( args, spec );
- NSMutableArray *mArray = [@[spec] mutableCopy];
- for ( ;; )
- {
- id tempSpec = va_arg( args, id );
- if (tempSpec == nil)
- break ;
- [mArray addObject:tempSpec];
- }
- va_end( args );
- CombinableSpec *combinableSpec = [[CombinableSpec alloc] init];
- combinableSpec.specs = [mArray copy];
- return combinableSpec;
- }
- - (BOOL)satisfy:(Product *)product
- {
- for (ProductSpec *spec in _specs) {
- if ([spec satisfy:product] == _shortcut) {
- return _shortcut;
- }
- }
- return !_shortcut;
- }
- @end
- @implementation AndSpec
- - (instancetype)init
- {
- self = [ super init];
- if (self) {
- self.shortcut = NO;
- }
- return self;
- }
- @end
- @implementation OrSpec
- - (instancetype)init
- {
- self = [ super init];
- if (self) {
- self.shortcut = YES;
- }
- return self;
- }
- @end
A large number of initialization methods are dazzling - [self findProducts:_products bySpec:[NotSpec spec:[AndSpec spec:[ColorSpec specWithColor:RED], [BelowWeightSpec specWithBelowWeight: 10 ], nil]]];
-
- Sixth Attempt: Using DSL
DSL can be introduced to improve the readability of the program and make the code more expressive. Let's start by adding some DSL: - static ProductSpec *COLOR(ProductColor color)
- {
- return [ColorSpec specWithColor:RED];
- }
- static ProductSpec *BELOWWEIGHT( float limit)
- {
- return [BelowWeightSpec specWithBelowWeight:limit];
- }
- static ProductSpec *AND(ProductSpec *spec1, ProductSpec *spec2)
- {
- return [AndSpec spec:spec1, spec2, nil];
- }
- static ProductSpec *OR(ProductSpec *spec1, ProductSpec *spec2)
- {
- return [OrSpec spec:spec1, spec2, nil];
- }
- static ProductSpec *NOT(ProductSpec *spec)
- {
- return [NotSpec spec:spec];
- }
This is how our code looks like - [self findProducts:_products bySpec:NOT(AND(COLOR(RED), BELOWWEIGHT( 10 )))];
-
- Seventh Attempt: Using a Lambda Expression
Blocks can be used to improve designs and enhance expressiveness. - - (NSArray *)findProducts:(NSArray *)products byBlock:(BOOL (^)())block
- {
- NSMutableArray *list = [@[] mutableCopy];
- for (Product *product in products) {
- if (block(product)) {
- [list addObject:product];
- }
- }
- return list;
- }
The code now looks like this - [self findProducts:_products byBlock:^BOOL(id p) { return [p color] == RED;}];
Construct DSL and reuse these blocks - ProductSpecBlock color(ProductColor color)
- {
- return ^BOOL(id p) { return [p color] == color;};
- }
- ProductSpecBlock weightBelow( float limit)
- {
- return ^BOOL(id p) { return [p weight] < limit;};
- }
- - ( void )test7_2
- {
- [self findProducts:_products byBlock:color(RED)];
- }
-
- Eighth attempt: Using NSPredicate
You can also use the standard library
- [self.products filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@ "weight > 10" ]];
Finish That's all for today's coding. This article was originally written by Horance, and I will implement it using OC. If we are not iOS Developers, there are still other attempts, such as generics. |