Паттерны проектирования, взгляд iOS разработчика. Часть 1. Стратегия
Содержание:
Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Напомню, что в этой серии статей, я разбираю книгу «Паттерны проектирования» Эрика и Элизабет Фримен. И сегодня мы изучим паттерн «Стратегия». Поехали.
Откуда растут ноги (и крылья)
Авторы книги рассказывают нам историю о создании приложения SimUDuck. Начнем с реализации начального состояния приложения: у нас есть абстрактный класс Duck
и два его наследника: MallardDuck
и RedheadDuck
. Тут же мы сталкиваемся с первой сложностью: в Objective-C и Swift нет абстрактных классов.
Выходим из ситуации теми инструментами, что есть: для Objective-C объявляем обычный класс Duck
(в нем будут реализации по умолчанию) и к нему добавляем протокол AbstractDuck
(в нем будут абстрактные методы, которые нужно реализовать в наследниках). Выглядит это так:
// Objective-C
@protocol AbstractDuck
- (void)display;
@end
@interface Duck : NSObject
- (void)quack;
- (void)swim;
@end
Соответственно наследники будут такими:
// Objective-C
@interface MallardDuck : Duck
@end
@implementation MallardDuck
- (void)display {
}
@end
@interface RedheadDuck : Duck
@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
- (void)fly;
@end
@protocol QuackBehavior
- (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
@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;
@property (strong, nonatomic) id 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, если, конечно, надумаете его использовать в вашем приложении. :)