Паттерны проектирования, взгляд iOS разработчика. Часть 1. Стратегия


    Содержание:


    Часть 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, если, конечно, надумаете его использовать в вашем приложении. :)

    • +12
    • 12,5k
    • 6
    Поделиться публикацией

    Комментарии 6

      0

      Спасибо за статью. Я не писал ни на Objective-C и ни на Swift, но примеры для Swift смотрятся так, как будто это твой язык разработки.

        –1

        Спасибо, что читаете. :)


        Да, Swift для меня все-таки ближе, на Objective-C удается покодить редко, к сожалению. :(

        +1
        А на вот этот репозиторий вы смотрели? Или принципиально хочется сделать самому?
          0

          Вот теперь, благодаря вам, смотрел.


          К сожалению в нем есть два недостатка:


          1. Отсутствует объяснение что к чему в каждом паттерне. Ведь не всегда, глядя на определенное количество классов/протоколов, можно понять что с чем и как взаимодействует. И самое главное — почему именно так.


          2. Примеры только на Swift, а я в своих статья разбираюсь еще и с Objective-C.
          +1
          Спасибо за статью, но есть замечание:

          Зачем все примеры по паттернам в упор не используют реальные примеры? Зацепите реальный пример с реальной привязкой к экранам, без этого выглядит как в анекдоте: «2+2 = 4, понятно? теперь решите уравнение 3x^2 + 5x + 1 = 0»
            0

            Согласен с вами на 100%. Но я эту серию постов пишу больше для того, чтоб разобраться с общим принципом работы каждого паттерна и синтаксисом обоих языков.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое