📜 ⬆️ ⬇️

Strategy (Translated from the English chapter of "Strategy" from the book "Pro Objective-C Design Patterns for iOS" by Carlo Chung)

Do you remember when you last completed a block of code with many different algorithms and used spaghetti from if-else / switch-case conditions to determine which one to use. Algorithms could be a set of functions / methods of similar classes that solve similar problems. For example, you have a procedure for validating input data. The data itself can be of any type (for example, CGFloat , NSString , NSInteger , etc.). Each data type requires different validation algorithms. If you could encapsulate each algorithm as an object, then you could not use a group of if-else / switch-case statements to check the data and determine which algorithm is needed.

In object-oriented programming, you can isolate related algorithms into different classes of strategies. The design pattern that is used in such cases is called Strategy. In this chapter, we will discuss the concepts and key features of the Strategy pattern. We will also design and implement several classes for data validation in the form of strategies for validating the input of an UITextField text field UITextField later in this chapter.

What is the Strategy pattern?


One of the key roles in this pattern is played by the strategy class, which declares a common interface for all supported algorithms. There are also specific classes of strategies that implement the algorithms using the strategy interface. The context object is configured using an instance of a specific policy object. The context object uses the strategy interface to invoke the algorithm defined in a particular strategy class. Their relationships are illustrated in the class diagram in Figure 19-1.
')
image
Figure 19-1. Class structure structure Strategy

A group or hierarchy of related algorithms in the form of ConcreteStrategy classes (A, B, and C) share a common algorithmInterface , so the Context can access different variants of the algorithms using the same interface.

Note. Pattern Strategy : defines a family of algorithms, encapsulates each of them, and makes them interchangeable. The strategy allows algorithms to change independently of the customers who use them. *

The original definition given in GoF’s Design Patterns (Addison-Wesley, 1994).

Context Instance can be configured using various ConcreteStrategy objects at run time. This can be viewed as a change in the "insides" of the Context object, since changes occur from within. Decorators (see Chapter 16, the Decorator pattern and my previous article), in contrast, change the "skin" of the object, since the modifications dock from the outside. Please refer to the section “Changing the" skin "of an object compared to changing the" viscera "in Chapter 16 (previous article) for more detailed information about the differences.

Pattern Strategy in Model-View-Controller

In the Model-View-Controller pattern, the controller determines when and how the view displays the data contained in the model. The view itself knows how to display something, but does not know that, as long as the controller does not indicate it. Working with another controller, but with the same appearance, the format of the output data may be the same, but the data types may be different according to different conclusions from the new controller. The controller in this case is a strategy for the object of the species. As we mentioned in previous chapters, the relationship between the controller and the view is based on the Strategy pattern.

When is the use of the Strategy pattern appropriate?


Using this pattern is advisable in the following cases:


Using data validation strategies using the example of the UITextField class


Let's create a simple example of implementing the Strategy pattern in the application. Suppose we need some UITextField object in our application that accepts user input; we will use the input results in our application later. We have a text entry field that accepts only letters, that is, a – z or A – Z, and we also have a field that accepts only numeric data, that is, 0–9. To make sure that the input in the fields is correct, each of them needs to have some kind of on-site data validation procedure that runs after the user finishes editing.

We can put the necessary data validation into the UITextField delegate object method, textFieldDidEndEditing: An instance of UITextField calls this method every time it loses focus. In this method we can make sure that only numbers are entered in the numeric field, and only letters are entered in the letter field. This method accepts as input a link to the current input field object (as a textField parameter), but which of these two objects exactly?

Without the STRAGUE pattern, we would come up with code similar to that shown in Listing 19-1.

Listing 19-1. Typical script to check the contents of a UITextField in the delegate method of textFieldDidEndEditing
 - (void)textFieldDidEndEditing:(UITextField *)textField { if (textField == numericTextField) { //  [textField text]  , //    } else if (textField == alphaTextField) { //  [textField text]  , //      } } 


Of course, there can be more conditional statements if there are more input fields for different data. We could make the code more manageable if we got rid of all these conditional expressions, which could greatly simplify our life in the future with the support of the code.

Tip: If there are many conditional statements in your code, this may mean that they need to be refactored and separated into separate objects of the Strategy.

Now our goal is to take on this verification code and scatter it across various classes of Strategies so that it can be reused in the delegate and other methods. Each of our classes takes a string from the input field, then checks it based on the required strategy, and finally returns a value of type BOOL and an instance of NSError if the check failed. The returned NSError object will help determine why the check was not successful. Since the verification of both numeric and alphabetic inputs are related to each other (they have the same type of input and output), they can be combined with one interface. Our set of classes is shown in the class diagram in Figure 19-2.

image
Figure 19–2. The class diagram shows the relationship between the CustomTextField and its associated strategies.

We will declare this interface not as a protocol, but as an abstract base class. The abstract base class is more convenient in this case, because it is easier to refactor the behavior common to all specific classes of strategies. Our abstract base class will look like Listing 19–2.

Listing 19–2. Class declaration InputValidator in InputValidator.h
 @interface InputValidator : NSObject { } //      - (BOOL) validateInput:(UITextField *)input error:(NSError **) error; @end 


The validateInput: error: method takes a link to a UITextField as an input parameter, so it can check everything in the input field and return the BOOL value as a result of the check. The method also accepts a reference to a pointer to an NSError . When an error occurs (that is, the method could not verify the input), the method will create an instance of NSError and assign it to a pointer, therefore, in whatever context the verification class is used, it is always possible to get more detailed error information from this object.

The default implementation of this method only sets the error pointer to nil and returns NO , as shown in Listing 19-3.

Listing 19–3. The default implementation of the class InputValidator in InputValidator.m
 #import "InputValidator.h" @implementation InputValidator //      - (BOOL) validateInput:(UITextField *)input error:(NSError **) error { if (error) { *error = nil; } return NO; } @end 


Why we did not use NSString as an input parameter? In this case, any action inside the strategy object will be one-sided. This means that the validator will simply do the check and return the result without modifying the original value. With an input parameter of type UITextField we can combine the two approaches. Our scan objects will be able to change the original value of the text field (for example, by deleting the wrong characters) or simply view the value without changing it.

Another question - why don't we just throw an NSException exception if the check fails? This is because throwing your own exception and intercepting it in a try-catch block in the Cocoa Touch framework is a very resource-intensive operation and is not recommended (but try-catch system exceptions are another matter). It is relatively cheaper to return an NSError object, as recommended in the Cocoa Touch Developer's Guide. If we look at the Cocoa Touch framework documentation, we notice that there are many APIs that return an instance of NSError when an abnormal situation occurs. A common example is one of the NSFileManager methods, (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error . If an error occurs, when NSFileManager tries to move a file from one place to another, it will create a new instance of NSError that describes the problem. The caller may use the information contained in the returned NSError object for further error handling. Thus, the purpose of the NSError object in our method is to provide information about a failure in operation.

Now we have defined how a good input validation class should behave. Now we can do the creation of this verifier. Let's first create the one for entering numbers, as shown in Listing 19-4.

Listing 19–4. NumericInputValidator class declaration in NumericInputValidator.h
 #import "InputValidator.h" @interface NumericInputValidator : InputValidator { } //  ,  ,     // ,   0-9 - (BOOL) validateInput:(UITextField *)input error:(NSError **) error; @end 


NumericInputValidator inherits from the abstract base class InputValidator and overrides its validateInput: error: method. We declare the method again to emphasize that this subclass implements or redefines it. This is not mandatory, but is good practice.

The implementation of the method is shown in Listing 19-5.

Listing 19-5. Implementing the NumericInputValidator Class in NumericInputValidator.m
 #import "NumericInputValidator.h" @implementation NumericInputValidator - (BOOL) validateInput:(UITextField *)input error:(NSError**) error { NSError *regError = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[0-9]*$" options:NSRegularExpressionAnchorsMatchLines error:®Error]; NSUInteger numberOfMatches = [regex numberOfMatchesInString:[input text] options:NSMatchingAnchored range:NSMakeRange(0, [[input text] length])]; //   , //     NO if (numberOfMatches == 0) { if (error != nil) { NSString *description = NSLocalizedString(@"Input Validation Failed", @""); NSString *reason = NSLocalizedString(@"The input can contain only numerical values", @""); NSArray *objArray = [NSArray arrayWithObjects:description, reason, nil]; NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey, NSLocalizedFailureReasonErrorKey, nil]; NSDictionary *userInfo = [NSDictionary dictionaryWithObjects:objArray forKeys:keyArray]; *error = [NSError errorWithDomain:InputValidationErrorDomain code:1001 userInfo:userInfo]; } return NO; } return YES; } @end 


The implementation of the validateInput:error: method focuses mainly on two aspects:

  1. It checks the number of coincidences of numerical data in the input field with the previously created NSRegularExpression object. The regular expression we used is “^ [0–9] * $”. It means that from the beginning of the entire line (indicated by “^”) and the end (indicated by “$”), there must be more or more characters (indicated by “*”) from the set that contains only numbers (indicated by “[0–9] ").
  2. If there is no match at all, it creates a new NSError object that contains the message “The input can contain only numerical values” and assigns it to the input pointer to the NSError . It then finally returns a BOOL value indicating the success or failure of the operation. The error is associated with the special code 1001 and the specific value of the error domain, defined in the header file of the InputValidator class InputValidator approximately as shown below:
     static NSString * const InputValidationErrorDomain = @"InputValidationErrorDomain"; 

The brother of the NumericInputValidator class, which checks for the presence of only letters in the input, called AlphaInputValidator , contains a similar algorithm for checking the content of the input field. AlphaInputValidator overrides the same method as NumericInputValidator . Obviously, this algorithm verifies that the input string contains only letters, as shown in Listing 19-6.

Listing 19-6. Implementing the AlphaInputValidator Class in AlphaInputValidator.m
 #import "AlphaInputValidator.h" @implementation AlphaInputValidator - (BOOL) validateInput:(UITextField *)input error:(NSError**) error { NSError *regError = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[a-zA-Z]*$" options:NSRegularExpressionAnchorsMatchLines error:®Error]; NSUInteger numberOfMatches = [regex numberOfMatchesInString:[input text] options:NSMatchingAnchored range:NSMakeRange(0, [[input text] length])]; //   , //     NO if (numberOfMatches == 0) { if (error != nil) { NSString *description = NSLocalizedString(@"Input Validation Failed", @""); NSString *reason = NSLocalizedString(@"The input can contain only letters", @""); NSArray *objArray = [NSArray arrayWithObjects:description, reason, nil]; NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey, NSLocalizedFailureReasonErrorKey, nil]; NSDictionary *userInfo = [NSDictionary dictionaryWithObjects:objArray forKeys:keyArray]; *error = [NSError errorWithDomain:InputValidationErrorDomain code:1002 userInfo:userInfo]; } return NO; } return YES; } @end 


Our class AlphaInputValidator also a type of InputValidator and implements the validateInput: method. It has a brother- NumericInputValidator , a code structure and algorithm, except that it uses another regular expression in the NSRegularExpression object, and the error code and message are specific to literal checking. The regular expression we use to test the letters is “^ [a-zA-Z] * $”. It is similar to the expression for his fellow numeric checker, except that the set of valid characters contains letters of both lower and upper case. As we can see, in both versions a lot of duplicate code. Both algorithms have a similar structure; you can refactor a structure into a template method (see chapter 18) into an abstract base class. Specific subclasses of InputValidator can override the primitive operations defined in InputValidator to return unique information to the template algorithm — for example, a regular expression and various construction attributes of an NSError object, etc. I will leave this to you as an exercise.

We now have test classes ready for use in the application. However, UITextFiel d does not know about them, so we need our own version of UITextField , which understands everything. We will create a subclass of UITextField that contains a reference to InputValidator and a validate method, as shown in Listing 19-7.

Listing 19-7. Class declaration CustomTextField in CustomTextField.h
 #import "InputValidator.h" @interface CustomTextField : UITextField { @private InputValidator *inputValidator_; } @property (nonatomic, retain) IBOutlet InputValidator *inputValidator; - (BOOL) validate; @end 


CustomTextField contains a property that holds ( retain ) a reference to InputValidator . When its validate method is called, it uses a reference to InputValidator to start the validation. We can see this in the implementation shown in Listing 19-8.

Listing 19-8. Implementing the CustomTextField class in CustomTextField.m
 #import "CustomTextField.h" @implementation CustomTextField @synthesize inputValidator=inputValidator_; - (BOOL) validate { NSError *error = nil; BOOL validationResult = [inputValidator_ validateInput:self error:&error]; if (!validationResult) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[error localizedDescription] message:[error localizedFailureReason] delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", @"") otherButtonTitles:nil]; [alertView show]; [alertView release]; } return validationResult; } - (void) dealloc { [inputValidator_ release]; [super dealloc]; } @end 


In the validate method, a message is sent [inputValidator_ validateInput:self
error:&error]
[inputValidator_ validateInput:self
error:&error]
[inputValidator_ validateInput:self
error:&error]
inputValidator_ link. The beauty of the pattern is that CustomTextField does not need to know what type of InputValidator it uses or any details of the algorithm. Therefore, if in the future we add some new InputValidator , the CustomTextField object will use the new InputValidator as well.

So, all the preparatory work done. Suppose the client is a UIViewController , which implements the UITextFieldDelegate protocol and contains two IBOutlets type CustomTextField , as shown in Listing 19–9.

Listing 19–9. Declaring the StrategyViewController class in StrategyViewController.h
 #import "NumericInputValidator.h" #import "AlphaInputValidator.h" #import "CustomTextField.h" @interface StrategyViewController : UIViewController <UITextFieldDelegate> { @private CustomTextField *numericTextField_; CustomTextField *alphaTextField_; } @property (nonatomic, retain) IBOutlet CustomTextField *numericTextField; @property (nonatomic, retain) IBOutlet CustomTextField *alphaTextField; @end 


We decided to allow the controller to implement the delegate (void)textFieldDidEndEditing:(UITextField *)textField and put the check there. This method will be called each time the value in the input field changes and the focus is lost. When the user finishes input, our CustomTextField class will call this delegate method, illustrated in Listing 19-10.

Listing 19-10. Client code defined in the textFieldDidEndEditing: delegate method that validates the CustomTextField instance using a strategy object (InputValidator)
 @implementation StrategyViewController @synthesize numericTextField, alphaTextField; // ... //    // ... #pragma mark - #pragma mark UITextFieldDelegate methods - (void)textFieldDidEndEditing:(UITextField *)textField { if ([textField isKindOfClass:[CustomTextField class]]) { [(CustomTextField*)textField validate]; } } @end 


When calling textFieldDidEndEditing: when editing in one of the fields is completed, the method checks that the textField object belongs to the CustomTextField class. If so, he sends a validate message to him to start the process of checking the entered text. As we can see, we no longer need these conditional statements. Instead, we have a much simpler code for the same purpose. Except for the additional verification that the textField object is a CustomTextField type, there is nothing more complicated.

But wait a minute. Something doesn't look good. How could we assign the correct InputValidator numericTextField_ and alphaTextField_ defined in StrategyViewController ? Both input fields are declared as IBOutlet in Listing 19–9. We can pick them up in the Interface Builder view controller through IBOutlet , as we do with other buttons and so on. Similarly, in the declaration of the CustomTextField class in Listing 19-7, its inputValidator property inputValidator also IBOutlet , which means that we can assign an instance of InputValidator to the *TextField object in Interface Builder, too.Thus, everything can be constructed by using Interface Builder reference connections if you declare certain class properties as IBOutlet. For a more detailed discussion of how to use custom Interface Builder objects, refer to “Using CoordinatingControllerInterface Builder” in Chapter 11, which talks about the Mediator pattern.

Conclusion


In this chapter, we discussed the concepts of the Strategy pattern and how this pattern can be used for clients to use various related algorithms. An example of the implementation of input checks for custom UITextFieldshows how various validation classes can change the "insides" of an object. Pattern Strategy is somewhat similar to the Pattern Decorator (Chapter 16 and my previous article). Decorators extend the object's behavior from the outside while various strategies are encapsulated inside the object. As they say, decorators change the "skin" of the object, and strategies - "insides".

In the next chapter we will see another pattern, which is also associated with the encapsulation of algorithms. The encapsulated algorithm is mainly used for deferred execution of a command as a separate object.

Source: https://habr.com/ru/post/222355/


All Articles