Стратегия (Перевод с английского главы «Strategy» из книги «Pro Objective-C Design Patterns for iOS» Carlo Chung)

в 19:45, , рубрики: Без рубрики

Помните ли вы, когда вы в последний раз начиняли блок кода множеством разных алгоритмов и использовали спагетти из условий if-else / switch-case, чтобы определить, какой именно из них использовать. Алгоритмы могли представлять собой набор функций/методов похожих классов, которые решают схожие проблемы. К примеру, у вас есть процедура для проверки входных данных. Сами данные могут быть любых типов (например, CGFloat, NSString, NSInteger и прочее). Каждый из типов данных требует различных алгоритмов проверки. Если бы вы могли инкапсулировать каждый алгоритм в виде объекта, то можно было бы не использовать группу операторов if-else / switch-case для проверки данных и определения, какой из алгоритмов нужен.

В объектно-ориентированном программировании вы можете выделить связанные алгоритмы в различные классы стратегий. Паттерн проектирования, который применяется в таких случаях, называется Стратегия. В этой главе мы обсудим концепции и ключевые возможности паттерна Стратегия. Мы также спроектируем и реализуем несколько классов для проверки данных в виде стратегий для валидации ввода объекта текстового поля UITextField позже в этой главе.

Что собой представляет паттерн Стратегия?

Одну из ключевых ролей в этом паттерне играет класс стратегии, который объявляет общий интерфейс для всех поддерживаемых алгоритмов. Есть также конкретные классы стратегий, которые реализуют алгоритмы, используя интерфейс стратегии. Объект контекста конфигурируется с помощью экземпляра конкретного объекта стратегии. Объект контекста использует интерфейс стратегии для вызова алгоритма, определенного в конкретном классе стратегии. Их отношения проиллюстрированы на диаграмме классов на рисунке 19–1.

image
Рисунок 19–1. Структура классов паттерна Стратегия

Группа или иерархия связанных алгоритмов в форме классов ConcreteStrategy (A, B и C) разделяют общий algorithmInterface, поэтому Context может получить доступ к разным вариантам алгоритмов с помощью одного и того же интерфейса.

Примечание. Паттерн Стратегия: определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет алгоритмам меняться независимо от клиентов, которые их используют.*

Исходное определение, данное в книге «Паттерны проектирования» GoF (Addison-Wesley, 1994).

Экземпляр Context может быть сконфигурирован с помощью различных объектов ConcreteStrategy во время выполнения. Можно это рассматривать как изменение «внутренностей» объекта Context, так как изменения происходят изнутри. Декораторы (смотри главу 16, паттерн Декоратор и мою предыдущую статью), в противовес, изменяют «шкуру» объекта, так как модификации пристыковываются извне. Пожалуйста, обращайтесь к разделу «Изменение «шкуры» объекта в сравнении с изменением «внутренностей»» в главе 16 (предыдущая статья) за более детальной информацией о различиях.

Паттерн Стратегия в Модель-Вид-Контроллер

В паттерне Модель-Вид-Контроллер контроллер определяет, когда и как виду отображать данные, содержащиеся в модели. Сам вид знает, как отобразить что-то, но не знает, что, пока контроллер ему не укажет. Работая с другим контроллером, но при том же виде, формат выводимых данных может быть тем же, но типы данных могут быть другими в соответствии с другим выводами от нового контроллера. Контроллер в этом случае является как бы стратегией для объекта вида. Как мы упоминали в предыдущих главах, отношения между контроллером и видом основаны на паттерне Стратегия.

Когда уместно использование паттерна Стратегия?

Использование этого паттерна целесообразно в следующих случаях:

  • В логике класса используется множество условных операторов для выбора нужного поведения. Вы можете переместить условный код в отдельный класс стратегии.
  • Вам нужны разные варианты алгоритма.
  • Вам бы не хотелось выставлять наружу сложные и узко специфичные структуры данных (клиентам).

Применение стратегий проверки данных на примере класса UITextField

Давайте создадим простой пример реализации паттерна Стратегия в приложении. Предположим, что нам нужен некий объект UITextField в нашем приложении, который принимает ввод пользователя; мы будем использовать результаты ввода в нашем приложении позже. У нас есть поле текстового ввода, которое принимает только буквы, то есть a–z или A–Z, а также у нас есть поле, которое принимает только числовые данные, то есть 0–9. Чтобы убедиться, что ввод в полях верен, каждому из них нужно иметь какую-то процедуру проверки данных на месте, запускаемую после того, как пользователь заканчивает редактирование.

Мы можем поместить необходимую проверку данных в метод объекта делегата UITextField, textFieldDidEndEditing:. Экземпляр UITextField вызывает этот метод каждый раз, когда теряет фокус. В этом методе мы можем убедиться в том, что в цифровом поле введены только цифры, а в буквенном — только буквы. Этот метод принимает на входе ссылку на текущий объект поля ввода (в виде параметра textField), но какой именно это из двух объектов?

Без паттерна Страгии мы бы пришли к коду, подобному показанному в листинге 19–1.

Листинг 19–1. Типичный сценарий проверки содержимого UITextField в методе делегата textFieldDidEndEditing

- (void)textFieldDidEndEditing:(UITextField *)textField
{
    if (textField == numericTextField)
    {
        // проверяем [textField text] и убеждаемся,
        // что значение цифровое
    }
    else if (textField == alphaTextField)
    {
        // проверяем [textField text] и убеждаемся,
        // что значение содержит только буквы
    }
}

Условных операторов, конечно, может быть и больше, если будет больше полей ввода для разных данных. Мы могли бы сделать код более управляемым, если бы избавились от всех этих условных выражений, что могло бы в будущем сильно упростить нам жизнь при поддержке кода.

Совет: Если в вашем коде много условных операторов, то это может означать, что их нужно отрефакторить и выделить в отдельные объекты Стратегии.

Теперь наша цель — взяться за этот код проверки и раскидать его по различным классам Стратегий, чтобы можно было его повторно использовать в делегате и других методах. Каждый из наших классов берет строку из поля ввода, затем проверяет его, основываясь на требуемой стратегии, и в конце возвращает значение типа BOOL и экземпляр NSError, если проверка провалилась. Возвращенный объект NSError поможет определить, из-за чего именно проверка не была успешна. Поскольку проверка и цифрового, и буквенного ввода связаны друг с другом (у них одинаковые типа на входе и выходе), их можно объединить одним интерфейсом. Наш набор классов показан на диаграмме классов на рисунке 19–2.

image
Рисунок 19–2. Диаграмма классов показывает отношения между CustomTextField и связанными с ним стратегиями

Мы объявим этот интерфейс не в виде протокола, а в виде абстрактного базового класса. Абстрактный базовый класс более удобен в данном случае, потому что проще рефакторить общее для всех конкретных классов стратегий поведение. Наш абстрактный базовый класс будет выглядеть, как показано в листинге 19–2.

Листинг 19–2. Объявление класса InputValidator в InputValidator.h

@interface InputValidator : NSObject
{
}
// Заглушка для любой стратегии проверки
- (BOOL) validateInput:(UITextField *)input error:(NSError **) error;

@end

Метод validateInput: error: принимает ссылку на UITextField в качестве входного параметра, поэтому он может проверить все, что находится в поле ввода, и возвращает значение BOOL как результат проверки. Метод также принимает ссылку на указатель на NSError. Когда произошла какая-то ошибка (то есть метод не смог проверить правильность ввода), метод создаст экземпляр NSError и присвоит его указателю, поэтому, в каком бы контексте не использовался класс проверки, всегда есть возможность получить более детальную информацию об ошибке из этого объекта.

Реализация этого метода по умолчанию только лишь устанавливает указатель на ошибку в nil и возвращает NO, как показано в листинге 19–3.

Листинг 19–3. Реализация по умолчанию класса InputValidator в InputValidator.m

#import "InputValidator.h"

@implementation InputValidator

// Заглушка для любой стратегии проверки
- (BOOL) validateInput:(UITextField *)input error:(NSError **) error
{
    if (error)
    {
        *error = nil;
    }
    return NO;
}
@end

Почему мы не использовали NSString в качестве входного параметра? В этом случае любое действие внутри объекта стратегии будет односторонним. Это значит, что валидатор просто сделает проверку и вернет результат без модификации исходного значения. С входным параметром типа UITextField мы можем объединить два подхода. Наши объекты проверки будут иметь возможность изменить исходное значение текстового поля (например, удалив неправильные символы) или просто просмотреть значение без его изменения.

Другой вопрос – почему бы нам просто не бросить исключение NSException, если проверка провалилась? Это потому, что выброс собственного исключения и перехват его в блоке try-catch во фреймворке Cocoa Touch является очень ресурсоемкой операцией и не рекомендуется (но try-catch системные исключения – это совсем другое дело). Относительно дешевле вернуть объект NSError, что рекомендовано в Cocoa Touch Developer’s Guide. Если мы посмотрим на документацию фреймворка Cocoa Touch, мы заметим, что есть множество API, которые возвращают экземпляр NSError, когда возникает какая-то ненормальная ситуация. Распространенный пример – это один из методов NSFileManager, (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error. Если возникает ошибка, когда NSFileManager пытается переместить файл из одного места в другое, он создаст новый экземпляр NSError, который описывает проблему. Вызывающий метод может использовать информацию, содержащуюся в возвращенном объекте NSError для дальнейшей обработки ошибок. Таким образом, цель объекта NSError в нашем методе – это обеспечение информации об отказе в работе.

Теперь мы определили, как должен вести себя хороший класс проверки ввода. Сейчас мы можем заняться созданием настоящего проверяющего. Давайте создадим сначала тот, что для ввода чисел, как показано в листинге 19–4.

Листинг 19–4. Объявление класса NumericInputValidator в NumericInputValidator.h

#import "InputValidator.h"

@interface NumericInputValidator : InputValidator
{
}

// Метод проверки, который убеждается, что ввод содержит только
// цифры, то есть 0-9
- (BOOL) validateInput:(UITextField *)input error:(NSError **) error;

@end

NumericInputValidator наследует от абстрактного базового класса InputValidator и переопределяет его метод validateInput: error:. Мы объявляем метод заново, чтобы подчеркнуть, что данный подкласс реализует или переопределяет его. Это не обязательно, но является хорошей практикой.

Реализация метода дана в листинге 19–5.

Листинг 19–5. Реализация класса NumericInputValidator в 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:&regError];
    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

Реализация метода validateInput:error: фокусируется главным образом на двух аспектах:

  1. Он проверяет количество совпадений численных данных в поле ввода с предварительно созданным объектом NSRegularExpression. Регулярное выражение, которое мы использовали, — это «^[0–9]*$». Он означает, что с начала всей строки (обозначено «^») и конца (обозначено «$»), должно быть 0 и более символов (обозначено «*») из набора, который содержит только цифры (обозначено «[0–9]»).
  2. Если совпадений нет вообще, то он создает новый объект NSError, который содержит сообщение «The input can contain only numerical values» и присваивает его входному указателю на NSError. Затем он наконец возвращает значение типа BOOL, указывающее на успех или неуспех операции. Ошибка ассоциирована с особым кодом 1001 и особым значением домена ошибки, определенным в заголовочном файле класса InputValidator примерно так, как показано ниже:
    static NSString * const InputValidationErrorDomain = @"InputValidationErrorDomain";
    

Брат класса NumericInputValidator, который проверяет наличие только букв во вводе, называемый AlphaInputValidator, содержит похожий алгоритм для проверки контента поля ввода. AlphaInputValidator переопределяет тот же метод, что и NumericInputValidator. Очевидно, что этот алгоритм проверяет, что входная строка содержит только буквы, как показано в листинге 19–6.

Листинг 19–6. Реализация класса AlphaInputValidator в 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:&regError];
    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

Наш класс AlphaInputValidator также является разновидностью InputValidator и реализует метод validateInput:. Он имеет похожие на брата, NumericInputValidator, структуру кода и алгоритм, за исключением того, что использует другое регулярное выражение в объекте NSRegularExpression, и код ошибки и сообщение специфичны для буквенной проверки. Регулярное выражение, которое мы используем для проверки букв, — «^[a-zA-Z]*$». Оно похоже на выражение для его собрата по числовой проверке, кроме того, что набор допустимых символов содержит буквы и нижнего, и верхнего регистра. Как мы видим, в обеих версиях много дублирующегося кода. У обоих алгоритмов похожая структура; вы можете отрефакторить структуру в шаблонный метод (смотри главу 18) в абстрактный базовый класс. Конкретные подклассы InputValidator могут переопределить примитивные операции, определенные в InputValidator, чтобы вернуть уникальную информацию шаблонному алгоритму – например, регулярное выражение и различные атрибуты конструирования объекта NSError и т. д. Я оставлю вам это в качестве упражнения.

Сейчас у нас уже есть классы проверки, готовые к использованию в приложении. Однако UITextField не знает о них, поэтому нам нужна собственная версия UITextField, которая все понимает. Мы создадим подкласс UITextField, который содержит ссылку на InputValidator и метод validate, это показано в листинге 19–7.

Листинг 19–7. Объявление класса CustomTextField в CustomTextField.h

#import "InputValidator.h"

@interface CustomTextField : UITextField
{
    @private
    InputValidator *inputValidator_;
}

@property (nonatomic, retain) IBOutlet InputValidator *inputValidator;

- (BOOL) validate;

@end

CustomTextField содержит свойство, которое удерживает (retain) ссылку на InputValidator. Когда вызывается его метод validate, он использует ссылку на InputValidator, чтобы начать проверку. Мы можем увидеть это в реализации, показанной в листинге 19–8.

Листинг 19–8. Реализация класса CustomTextField в 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

В методе validate посылается сообщение [inputValidator_ validateInput:self
error:&error]
ссылке inputValidator_. Красота паттерна в том, что CustomTextField-у не нужно знать, какого типа InputValidator он использует или какие-либо детали алгоритма. Поэтому если в будущем мы добавим какой-то новый InputValidator, объект CustomTextField будет использовать новый InputValidator так же.

Итак, все подготовительные работы сделаны. Допустим, клиентом является UIViewController, который реализует протокол UITextFieldDelegate и содержит два IBOutlets типа CustomTextField, как показано в листинге 19–9.

Листинг 19–9. Объявление класса StrategyViewController в 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

Мы решили позволить контроллеру реализовывать метод делегата (void)textFieldDidEndEditing:(UITextField *)textField и поместить проверку туда. Этот метод будет вызываться каждый раз, когда значение в поле ввода будет изменяться, а фокус будет потерян. Когда пользователь закончит ввод, наш класс CustomTextField вызовет этот метод делегата, проиллюстрировано в листинге 19–10.

Листинг 19–10. Клиентский код, определенный в методе делегата textFieldDidEndEditing:, который проверяет экземпляр CustomTextField с помощью объекта стратегии (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

При вызове textFieldDidEndEditing:, когда редактирование в одном из полей закончено, метод проверяет, что объект textField принадлежит к классу CustomTextField. Если так, то он посылает сообщение validate ему для запуска процесса проверки введенного текста. Как мы можем видеть, нам больше не нужны эти условные операторы. Вместо этого у нас есть гораздо более простой код для тех же целей. За исключением дополнительной проверки того, что объект textField является типом CustomTextField, больше ничего сложного нет.

Но подождите минутку. Кое-что выглядит не очень хорошо. Как бы мы могли присвоить корректные экземпляры InputValidator numericTextField_ и alphaTextField_, определенным в StrategyViewController? Оба поля ввода объявлены как IBOutlet в листинге 19–9. Мы можем подцепить их во вьюконтроллере Interface Builder через IBOutlet-ы, как мы делаем с другими кнопками и прочим. Аналогично в объявлении класса CustomTextField в листинге 19–7, его свойство inputValidator также IBOutlet, что означает, что мы можем присвоить экземпляр InputValidator объекту *TextField тоже в Interface Builder. Таким образом, все может быть сконструировано посредством использования ссылочных соединений Interface Builder, если вы объявите определенные свойства класса как IBOutlet. Для более детального обсуждения, как использовать кастомные объекты Interface Builder, обращайтесь к «Использование CoordinatingController в Interface Builder» в главе 11, где говорится о паттерне Медиатор.

Заключение

В этой главе мы обсудили концепции паттерна Стратегия и как можно задействовать этот паттерн для использования клиентами различных связанных алгоритмов. Пример реализации проверок ввода для кастомного UITextField показывает, как различные классы проверки могут изменить «внутренности» объекта. Паттерн Стратегия чем-то похож на паттерн Декоратор (глава 16 и моя предыдущая статья). Декораторы расширяют поведение объекта извне в то время, как различные стратегии инкапсулируются внутри объекта. Как говорят, декораторы изменяют «шкуру» объекта, а стратегии – «внутренности».

В следующей главе мы увидим другой паттерн, который тоже связан с инкапсуляцией алгоритмов. Инкапсулированный алгоритм в основном используется для отложенного выполнения команды в виде отдельного объекта.

Автор: WildCat2013

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js