
Содержание:
Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Часть 2. Наблюдатель
Напомню, что в этой серии статей, я разбираю книгу "Паттерны проектирования" Эрика и Элизабет Фримен. И сегодня мы изучим паттерн "Стратегия". Поехали.
Откуда растут ноги (и крылья)
Авторы книги рассказывают нам историю о создании приложения SimUDuck. Начнем с реализации начального состояния приложения: у нас есть абстрактный класс Duck и два его наследника: MallardDuck и RedheadDuck. Тут же мы сталкиваемся с первой сложностью: в Objective-C и Swift нет абстрактных классов.
Выходим из ситуации теми инструментами, что есть: для Objective-C объявляем обычный класс Duck (в нем будут реализации по умолчанию) и к нему добавляем протокол AbstractDuck (в нем будут абстрактные методы, которые нужно реализовать в наследниках). Выглядит это так:
// Objective-C @protocol AbstractDuck <NSObject> - (void)display; @end @interface Duck : NSObject - (void)quack; - (void)swim; @end
Соответственно наследники будут такими:
// Objective-C @interface MallardDuck : Duck <AbstractDuck> @end @implementation MallardDuck - (void)display { } @end @interface RedheadDuck : Duck <AbstractDuck> @end @implementation RedheadDuck - (void)display { } @end
В Swift это сделать немного проще: достаточно протокола и его расширения (в расширении можно некоторые методы протокола реализовать по умолчанию):
// Swift protocol Duck { func quack() func swim() func display() } extension Duck { func quack() { } func swim() { } }
И наследники:
// Swift class MallardDuck: Duck { func display() { } } class RedheadDuck: Duck { func display() { } }
Приложение развивается и у уток появляется возможность летать
Для этого соответствующий метод появляется в родительском классе Duck. И вскоре после этого выясняется, что есть еще один наследник — RubberDuck. Резиновые утки ведь не летают, а поскольку метод добавлен в родительский класс, то он будет доступен и для резиновых уток. В общем: ситуация оказалась не из простых. При дальнейшем расширении приложения будут возникать сложности с поддержкой функций полета (и не только с ней, с функцией кряканья та же история) и с другими видами уток (деревянных, например).
Сначала авторы книги предлагают решать проблему вынесением функций полета и кряканья в отдельные интерфейсы (для Objective-c и Swift — протоколы) Flyable и Quackable. Но этот вариант оказывается совсем не так хорош, каким кажется на первый взгляд. Малейшее изменение функции полета, которое должно быть применено ко всем летающим уткам влечет за собой внесение одного и того же кода во многих местах программы. Так что такое решение определенно не подходит.
(говоря о негодности этого варианта, авторы ссылаются на то, что в интерфейсах (для нас протоколах) нет реализаций по умолчанию, но это справедливо лишь для Objective-C, а вот в Swift реализации по умолчанию для полета и кряканья можно было бы написать в расширениях этих протоколов и переопределять эти функции только там где необходимо, а не везде)
Ну и к тому же, одна из главных целей паттерна — подменяемая реализация поведений во время выполнения, поэтому авторы предлагают выносить реализации поведений из класса Duck.
Для этого создадим протоколы FlyBehavior и QuackBehavior:
// Objective-C @protocol FlyBehavior <NSObject> - (void)fly; @end @protocol QuackBehavior <NSObject> - (void)quack; @end
// Swift protocol FlyBehavior { func fly() } protocol QuackBehavior { func quack() }
И конкретные классы реализующие эти протоколы: FlyWithWings и FlyNoWay для FlyBehavior, а также Quack, Squeak и MuteQuack для QuackBehavior (приведу пример для FlyWithWings, остальные реализуются очень схожим образом) :
// Objective-C @interface FlyWithWings : NSObject <FlyBehavior> @end @implementation FlyWithWings - (void)fly { // fly implementation } @end
// Swift class FlyWithWings: FlyBehavior { func fly() { // fly implementation } }
Делегирование наше все
Теперь мы, по сути, делегируем наше поведение любому другому классу, который реализует соответствующий интерфейс (протокол). Как сказать нашей утке каким должно быть ее поведение в полете и при кряканьи? Очень просто, добавляем в наш класс (в Swift — протокол) Duck два свойства:
// Objective-C @property (strong, nonatomic) id<FlyBehavior> flyBehavior; @property (strong, nonatomic) id<QuackBehavior> quackBehavior;
// Swift var flyBehavior: FlyBehavior { get set } var quackBehavior: QuackBehavior { get set }
Как видите у них не определен конкретный тип, определено лишь, что это класс подписанный на соответствующий протокол.
Методы fly и quack нашего родительского класса (или протокола) Duck заменим аналогичными:
// Objective-C - (void)performFly { [self.flyBehavior fly]; } - (void)performQuack { [self.quackBehavior quack]; }
// Swift func performFly() { flyBehavior.fly() } func performQuack() { quackBehavior.quack() }
Теперь наша утка просто делегирует свое поведение соответствующему поведенческому объекту. Как мы устанавливаем поведение каждой утке? Например при инициализации (пример для MallardDuck):
// Objective-C - (instancetype)init { self = [super init]; if (self) { self.flyBehavior = [[FlyWithWings alloc] init]; self.quackBehavior = [[Quack alloc] init]; } return self; }
// Swift init() { self.flyBehavior = FlyWithWings() self.quackBehavior = Quack() }
Наш паттерн готов :)
Заключение
В iOS разработке паттерн "Стратегия" вы можете встретить, например, в архитектуре MVP: в ней презентер является не чем иным как поведенческим объектом для вью-контроллера (вью-контроллер, как вы помните, только сообщает презентеру о действиях пользователя, а вот логику обработки данных определяет презентер), и наоборот: вью-контроллер — поведенческий объект для презентера (презентер лишь говорит "показать пользователю данные", но как именно они будут показаны — решит вью-контроллер). Также этот паттерн вы встретите и в VIPER, если, конечно, надумаете его использовать в вашем приложении. :)