Паттерны GoF

Список паттернов банды четырех – те самые паттерны из-за которых вас могут валить на собеседовании.

Порождающие паттерны

Структурные паттерны

Паттерны поведения

Паттерн Интерпретатор

Что входит

Паттерн Интерпретатор относится к Поведенческим паттернам проектирования. Данный паттерн позволяет реализовать свой язык программирования, путем работы с AST древом, вершины которого представляют из себя терминальные и нетерминальные выражения, реализующие метод Interpret, обеспечивающий функционал языка.

  • Терминальное выражение – например константа строки – “Hello World”
  • Нетерминальное выражение – например Print(“Hello World”), содержит Print и аргумент из Терминального выражения “Hello World”

В чем разница? Разница в том что интерпретирование на терминальных выражениях заканчивается, а для нетерминальных продолжается вглубь по всем входящим вершинам/аргументам. Если бы AST древо состояло только из нетерминальных выражений, то выполнение приложения никогда бы не завершилось, т.к. требуется некая конечность любого процесса, этой конечность и выступают терминальные выражения, они обычно содержат данные, например строки.

Пример AST древа ниже:


Dcoetzee, CC0, via Wikimedia Commons

Как можно увидеть, терминальные выражения – constant и variable, нетерминальные – остальные.

Что не входит

В реализацию Интерпретатора не входит парсинг строкового ввода языка в AST-древо. Достаточно реализовать классы терминальных, нетерминальных выражений, методы Interpret с аргументом Контекст на входе, оформить AST древо из выражений, запустить у корневого выражения метод Interpret. Контекст можно использовать для того чтобы хранить состояние приложения во время выполнения.

Реализация

В паттерне участвуют:

  • Клиент – отдает AST-древо и запускает Interpret(context) для корневой вершины (Client)
  • Контекст – содержит состояние приложения, передается выражениям при интерпретации (Context)
  • Абстрактное выражение – абстрактный класс содержащий метод Interpret(context) (Expression)
  • Терминальное выражение – конечное выражение, потомок абстрактного выражения (TerminalExpression)
  • Нетерминальное выражение – не конечное выражение, содержит указатели на вершины вглубь AST-древа, подчиненные вершины обычно влияют на результат интерпретации нетерминального выражения (NonTerminalExpression)

Пример Клиента на C#

        static void Main(string[] args)
        {
            var context = new Context();
            var initialProgram = new PerformExpression(
                new IExpression[] {
                    new SetExpression("alpha", "1"),
                    new GetExpression("alpha"),
                    new PrintExpression(
                        new IExpression[] {
                            new ConstantExpression("Hello Interpreter Pattern")
                        }
                    )
                }
            );
            System.Console.WriteLine(initialProgram.interpret(context));
        }
}

Пример Абстрактного выражения на C#

{
    String interpret(Context context);
}

Пример Терминального выражения на C# (Строковая константа)

{
    private String constant;

    public ConstantExpression(String constant) {
        this.constant = constant;
    }

    override public String interpret(Context context) {
        return constant;
    }
}

Пример Нетерминального выражения на C# (Запуск и конкатенация результатов подчиненных вершин, с использованием разделителя «;»

{
    public PerformExpression(IExpression[] leafs) : base(leafs) {
        this.leafs = leafs;
    }
    
    override public String interpret(Context context) {
        var output = "";
        foreach (var leaf in leafs) {
            output += leaf.interpret(context) + ";";
        }
        return output;
    }
}

Функционально сможешь?

Как известно все Тьюринг-полные языки эквивалентны. Можно ли перенести Объектно-Ориентированный паттерн на язык Функционального программирования?

Можно, для эксперимента возьмем ФП язык для веба под названием Elm. В Elm нет классов, но есть Записи (Records) и Типы (Types) поэтому в реализации участвуют следующие записи и типы:

  • Выражение – перечисление всех возможных выражений языка (Expression)
  • Подчиненное выражение – выражение являющееся подчиненным по отношению к Нетерминальному выражению (ExpressionLeaf)
  • Контекст – запись хранящая состояние приложения (Context)
  • Функции реализующие методы Interpret(context) – все необходимые функции реализующие функционал Терминальных, Нетерминальных выражений
  • Вспомогательные записи состояния Интерпретатора – необходимы для корректной работы Интерпретатора, хранят состояние Интерпретатора, контекст

Пример функции реализующей интерпретацию для всего набора возможных выражений на Elm:

  case input.expression of
    Constant text ->
      { 
        output = text, 
        context = input.context 
      }
    Perform leafs ->
      let inputs = List.map (\leaf -> { expressionLeaf = leaf, context = input.context } ) leafs in
        let startLeaf = { expressionLeaf = (Node (Constant "")), context = { variables = Dict.empty } } in
          let outputExpressionInput = List.foldl mergeContextsAndRunLeafs startLeaf inputs in
            {
              output = (runExpressionLeaf outputExpressionInput).output,
              context = input.context
            }
    Print printExpression ->
      run 
      { 
        expression = printExpression, 
        context = input.context 
      }
    Set key value ->
      let variables = Dict.insert key value input.context.variables in
      {
        output = "OK",
        context = { variables = variables }
      }
    Get key ->
      {
        output = Maybe.withDefault ("No value for key: " ++ key) (Dict.get key input.context.variables),
        context = input.context
      }

А парсить?

Парсинг исходного кода в AST-древо не входит в паттерн Интерпретатор, существует несколько подходов для парсинга исходного кода, но об этом как-нибудь в другой раз.
В реализации Интерпретатора для Elm я написал простейший парсер в AST-древо, состоящий из двух функций – парсинг вершины, парсинг подчиненных вершин.

parseLeafs state =
    let tokensQueue = state.tokensQueue in
        let popped = pop state.tokensQueue in
            let tokensQueueTail = tail state.tokensQueue in
                if popped == "Nothing" then
                    state
                else if popped == "Perform(" then
                    {
                        tokensQueue = tokensQueue,
                        result = (state.result ++ [Node (parse tokensQueue)])
                    }
                else if popped == ")" then
                    parseLeafs {
                        tokensQueue = tokensQueueTail,
                        result = state.result
                    }
                else if popped == "Set" then
                    let key = pop tokensQueueTail in
                        let value = pop (tail tokensQueueTail) in
                            parseLeafs {
                                tokensQueue = tail (tail tokensQueueTail),
                                result = (state.result ++ [Node (Set key value)])
                            }
                else if popped == "Get" then
                    let key = pop tokensQueueTail in
                        parseLeafs {
                            tokensQueue = tail tokensQueueTail,
                            result = (state.result ++ [Node (Get key)])
                        }
                else 
                    parseLeafs {
                        tokensQueue = tokensQueueTail,
                        result = (state.result ++ [Node (Constant popped)])
                    }

parse tokensQueue =
    let popped = pop tokensQueue in
        let tokensQueueTail = tail tokensQueue in
            if popped == "Perform(" then
                Perform (
                    parseLeafs {
                        tokensQueue = tokensQueueTail, 
                        result = []
                    }
                ).result
            else if popped == "Set" then
                let key = pop tokensQueueTail in
                    let value = pop (tail tokensQueueTail) in
                        Set key value
            else if popped == "Print" then
                Print (parse tokensQueueTail)
            else
                Constant popped

Ссылки

https://gitlab.com/demensdeum/patterns/-/tree/master/interpreter/elm
https://gitlab.com/demensdeum/patterns/-/tree/master/interpreter/csharp

Источники

https://en.wikipedia.org/wiki/Interpreter_pattern
https://elm-lang.org/
https://docs.microsoft.com/en-us/dotnet/csharp/

Как я не попал в мужика на шесте или история об удивительной изобретательности

В данной заметке я напишу о важности архитектурных решений при разработке, поддержке приложения, в условиях командной разработки.

Самодействующая салфетка профессора Люцифера Горгонзолы. Руб Голдберг

Во времена своей юности я работал над приложением для заказа такси. В проге можно было выбирать точку пикапа, точку дропа, рассчитывать стоимость поездки, тип тарифа, и собственно говоря, заказать такси. Приложение мне досталось на последнем этапе пред-запуска, после добавления нескольких фиксов приложение было выпущено в AppStore. Уже на том этапе вся команда понимала что реализована она очень плохо, паттерны проектирования не использовались, все компоненты системы были связаны намертво, в общем и целом, можно было ее записать в один большой сплошной класс (God object), ничего бы не изменилось, так как классы смешивали свои границы ответственности и в общей своей массе перекрывали друг друга мертвой сцепкой. Позже руководством было принято решение написать приложение с нуля, с использованием корректной архитектуры, что было выполнено и итоговый продукт был внедрен нескольким десяткам B2B клиентов.

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

Это был обычный рабочий день, от одного из заказчиков пришло задание немного доработать дизайн приложения – банально подвинуть на несколько пикселей вверх иконку в центре экрана выбора адреса пикапа. Что ж, профессионально оценив задачу в 10 минут я поднял иконку на 20 пикселей вверх, совершенно ничего не подозревая, я решил проверить заказ такси.

Что? Приложение больше не показывает кнопку заказа? Как это получилось?

Я не мог поверить своим глазам, после поднятия иконки на 20 пикселей приложение перестало показывать кнопку продолжения заказа. Откатив изменение я увидел кнопку снова. Что-то здесь было не так. Просидев 20 минут в дебаггере я немного устал от разматывания спагетти из вызовов перекрывающих друг друга классов, но обнаружил что *сдвигание картинки действительно меняет логику приложения*

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

Как это может происходить? Неужели *состояние* экрана зависит не от паттерна машины состояния, а от *представления* позиции мужика на шесте?

Все так и оказалось, при каждой отрисовке карты приложение *визуально тыкало* в середину экрана и проверяла что там, если там мужик на шесте то это значит что анимация сдвига карты закончилась и нужно показать кнопку. В случае когда мужика там нет — значит происходит сдвиг карты, и кнопку надо спрятать.

В примере выше прекрасно все, во первых это пример Машины Голдберга (заумные машины), во вторых пример нежелания разработчика как-то взаимодействовать с другими разработчиками в команде (попробуй разберись без меня), в третьих можно перечислить все проблемы по SOLID, паттернам (запахи кода), нарушение MVC и многое многое другое.

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

Ссылки

https://ru.wikipedia.org/wiki/Машина_Голдберга

https://ru.wikipedia.org/wiki/SOLID

https://refactoring.guru/ru/refactoring/smells

https://ru.wikipedia.org/wiki/Model-View-Controller

https://refactoring.guru/ru/design-patterns/state

Паттерн Фасад


Фасад относится к структурным паттернам проектирования. Он предоставляет единый интерфейс, обеспечивающий работу со сложными системами, позволяя клиентам не обладать деталями реализации о данных системах, упрощать таким образом свой код, реализовывать слабую связанность между клиентами и системами нижнего уровня. В GoF есть хороший пример Фасада – компилятор языков программирования, предоставляющий разным клиентам, преследующим разные цели, возможность сборки кода через единый интерфейс фасада-компилятора.

Источники

https://refactoring.guru/ru/design-patterns/facade
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

Паттерн Абстрактная Фабрика

Абстрактная фабрика – предоставляет интерфейс создания связанных объектов, без указания конкретных классов.

Мне большие нравится альтернативное название данного паттерна – Набор (Kit)

Он очень похож на Фабричный Метод, однако Абстрактные Фабрики должны описывать связь между создаваемыми объектами, иначе это уже просто антипаттерн God Object, создающий бессистемно все подряд.

Представим себе разработку AR фреймворка для очков, мы выводим на экране стрелки indoor навигации, иконки магазинов, интересных мест, окна и кнопки с информацией о каком-либо месте, в котором сейчас находится пользователь.

При этом нам нужна возможность кастомизировать внешний вид и поведение контролов AR окружения. Вот именно для этого случая нужно использовать паттерн Набор.

Напишем интерфейс Абстрактной Фабрики и Абстрактных Продуктов – родительских протоколов, элементов AR окружения:

protocol ARFactory {
    func arrow() -> ARArrow
    func icon() -> ARIcon
    func button() -> ARButton
    func window() -> ARWindow
}

protocol ARArrow {
    var image: { get }
    func handleSelection()
}

protocol ARIcon {
    var image: { get }
    var title: String
}

protocol ARButton {
    var title: String
    func handleSelection()
}

protocol ARWindow {
    var title: String
    var draw(canvas: Canvas)
}

Теперь разработчикам наборов нужно будет реализовать Конкретную Фабрику на основе интерфейса Абстрактной Фабрики, причем реализовать придется все элементы вместе, остальные части приложения смогут работать с фабрикой не меняя свой код.

Источники

https://refactoring.guru/ru/design-patterns/abstract-factory
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

Фабричный Метод

Паттерн Фабричный Метод относится к порождающим паттернам проектирования.
Данный паттерн описывает создание интерфейса для создания объекта конкретного класса. Вроде просто да?

В теории

Допустим мы разрабатываем фреймворк для работы с AR очками, при наклоне головы на бок, перед глазами пользователя должно появляться меню доступных приложений. Приложения будут разрабатывать сторонние компании, клиенты нашего фреймворка. Естественно мы не знаем какие именно приложения, значки, названия должны появляться, поэтому мы должны предоставить интерфейс для реализации значка и сопутствующей информации о приложении. Назовем его Продуктом:

protocol Product {
 var name: String { get }
 var image: Image { get }
 var executablePath: String { get }
}

Далее нам нужно предоставить интерфейс для того чтобы наши клиенты реализовали выдачу массива приложений своего Конкретного Продукта – массив значков приложений с названиями, который мы уже отрисуем во фреймворке.

Напишем этот интерфейс – интерфейс Создателя, содержащий Фабричный Метод, отдающий массив Продуктов.

protocol Creator {
 func factoryMethod() -> [Product]
}

На практике

Первым клиентом нашего AR фреймворка стала компания 7Б – ведущий поставщик софта для кофеварок в Гондурасе. Они хотят продавать очки дополненной реальности с возможностью заваривать кофе, проверять заполненность воды/зерен, указывать дорогу к их ближайшей кофеварке в режиме indoor карт.

Разработку софта они берут на себя, от нас требуется только предоставить документацию по интерфейсам Создателя и Продукта, для корректного вывода списка приложений и их дальнейшего запуска.

После передачи документации, компания 7Б используя интерфейс Создателя реализует Конкретного Создателя – класс возвращающий массив приложений-значков. Сами приложения-значки представляют из себя классы Конкретного Продукта имплементирующие интерфейс Продукта.

Пример кода Конкретных Продуктов:

class CoffeeMachineLocator: implements Product {
 let name = “7B Coffee Machine Locator v.3000”
 let image = Image.atPath(“images/locator.tga”)
 let executablePath = “CoffeeMachineLocator.wasm”
}

class iPuchinno: implements Product {
 let name = “iPuchinno 1.0.3”
 let image = Image.atPath(“images/puchino.pvrtc”)
 let executablePath = “neutron/ipuchBugFixFinalNoFreezeFixAlpha4.js”
}

Класс Конкретного Создателя, отдающий массив из двух приложений:

class 7BAppsCreator: implements Creator {
 func factoryMethod() -> [Product] {
  return [CoffeeMachineLocator(), iPuchinno()]
 }
}

После этого компания 7Б компилирует библиотеку Конкретных Продуктов, Конкретного Создателя и совмещает ее с нашим фреймворком, начинает продавать AR очки для своих кофеварок, доработок с нашей стороны не потребуется.

Источники

https://refactoring.guru/ru/design-patterns/command
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

Паттерн Команда

Паттерн Команда относится к поведенческим паттернам проектирования.

Это паттерн с которым я сижу дольше остальных, он настолько прост, что очень сложен. Но лично я нахожу прелесть самообучения в том что у тебя есть все время мира чтобы исследовать определенный вопрос под всеми углами.

Итак, в GoF применимость описывается достаточно лаконично и понятно:
Инкапсулирует запрос как объект, позволяя настраивать (parameterize) клиентов с разными запросами, использовать очереди, логировать запросы и осуществлять операции отмены.

Теперь реализуем простой вариант команды из описания:

string fakeTrumpsRequest = “SELECT * from Users where name beginsWith DonaldTrump”

Мы инкапсулировали запрос в объект класса строки, ей можно настраивать клиентов, добавлять команды в очередь, логировать, осуществлять отмену (с использованием паттерна “Снимок”)

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

Матчасть

Паттерн команда начинается с протокола Команды, который содержит единственный метод execute(). Дальше идет Конкретная Команда и Ресивер, КК реализует операцию над Ресивером, описывает связь между Ресивером и действием. Ничего непонятно? Мне тоже, но поехали дальше. Клиент создает экземпляр Конкретной Команды, связывает ее с Ресивером. Инвокер – объект который осуществляет процесс запуска Команды.

Теперь попробуем разобраться на примере, допустим мы хотим обновить myOS на телефоне myPhone, для этого мы запускаем приложение myOS_Update!, в нем нажимаем кнопку Update Now!, через 10 секунд система сообщит об успешном обновлении.

Клиентом в примере выше выступает приложение myOS_Update!, Инвокер это кнопка “Update Now!”, он запускает Конкретную Команду обновления системы с помощью метода execute(), которая обращается к Ресиверу – демону обновления операционной системы.

Пример использования

Допустим UI приложения myOS_Update! настолько хорош, что его решили продавать как отдельный продукт для предоставления интерфейса обновления других операционных систем. В таком случае мы реализуем приложение с поддержкой расширения через библиотеки, в библиотеках будут реализации Конкретных Команд, Ресиверов, оставим статичные/неизменяемые Инвокер, Клиент, протокол Команды.

Таким образом отпадает необходимость в осуществлении поддержки изменяемого кода, так как наш код останется неизменным, проблемы могут возникнут лишь при реализации на стороне клиентов, из-за ошибок в коде их Конкретных Команд и Ресиверов. Также в такой реализации отсутствует необходимость передавать исходный код основного приложения, то есть мы осуществили инкапсуляцию команд и взаимодействия UI с помощью паттерна Команда.

Источники

https://refactoring.guru/ru/design-patterns/command
https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

Паттерн Строитель

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

Применимость

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

Пример на C++ без строителя:

auto weapon = new Weapon(“Claws”);
monster->weapon = weapon;
auto health = new MonsterHealth(100);
monster->health = health;

Пример со строителем на C++:

                  .addWeapon(“Claws”)
                  .addHealth(100)
                  .build();

Однако в языках поддерживающих именованные аргументы (named arguments), необходимость использовать именно для этого случая отпадает.

Пример на Swift с использованием named arguments:

let monster = Monster(weapon: “Claws”, health: 100)

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

Взаимодействие с компонентами на разных этапах создания объекта. Также используя паттерн есть возможность обеспечить пошаговое создание объекта, при взаимодействии с другими компонентами системы. Скорее всего это очень полезно (?)

Критика

Конечно нужно *хорошенько* подумать стоит ли налаживать повсеместное использование паттерна в своем проекте. Языки с современным синтаксисом и продвинутым IDE нивелируют необходимость в использовании Строителя, в плане улучшения читаемости кода (см, пункт про именованные аргументы)
Нужно ли было использовать данный паттерн в 1994 году, на момент выпуска книги GoF? Скорее всего да, однако, судя по Open source кодовой базе тех лет, мало кто им пользовался.

Источники

https://refactoring.guru/ru/design-patterns/builder

Паттерн Композит

Паттерн Композит относится к структурным паттернам проектирования, в отечественных источниках он известен как “Компоновщик”.
Допустим мы разрабатываем приложение – фотоальбом. Пользователь может создавать папки, добавлять туда фото, производить прочие манипуляции. Обязательно нужна возможность показывать количество файлов в папках, общее количество всех файлов и папок.
Очевидно что нужно использовать дерево, но как реализовать архитектуру древа, с простым и удобным интерфейсом? На помощь приходит паттерн Композит.

Sheila in Moonducks

Далее в Directory реализуем метод dataCount() – путем прохода по всем элементам лежащим в массиве компонентов, сложив все их dataCount’s.
Все готово!
Ниже пример на Go:
package main

import "fmt"

type component interface {

dataCount() int

}

type file struct {

}

type directory struct {

c []component

}

func (f file) dataCount() int {

return 1

}

func (d directory) dataCount() int {

var outputDataCount int = 0

for _, v := range d.c {
outputDataCount += v.dataCount()
}

return outputDataCount

}

func (d *directory) addComponent(c component) {

d.c = append(d.c, c)

}

func main() {

var f file
var rd directory
rd.addComponent(f)
rd.addComponent(f)
rd.addComponent(f)
rd.addComponent(f)

fmt.Println(rd.dataCount())

var sd directory
sd.addComponent(f)

rd.addComponent(sd)
rd.addComponent(sd)
rd.addComponent(sd)

fmt.Println(sd.dataCount())
fmt.Println(rd.dataCount())

}

Источники

https://refactoring.guru/ru/design-patterns/composite

Паттерн Адаптер

Benjamín Núñez González

Паттерн Адаптер относится к структурным паттернам проектирования.

Адаптер обеспечивает конвертацию данных/интерфейсов между двумя классами/интерфейсами.

Допустим мы разрабатываем систему определения цели покупателя в магазине на основе нейросетей. Система получает видеопоток с камеры магазина, определяет покупателей по их поведению, классифицирует по группам. Виды групп – пришел купить (потенциальный покупатель), просто посмотреть (зевака), пришел что-то украсть (вор), пришел сдать товар (недовольный покупатель), пришел пьяный/под кайфом (потенциальный дебошир).

Как все опытные разработчики, мы находим готовую нейросеть которая умеет классифицировать виды обезьян в клетке по видеопотоку, которую любезно выложил в свободный доступ зоологический институт берлинского зоопарка, переучиваем ее на видеопотоке из магазина и получаем рабочую state-of-the-art систему.

Есть лишь небольшая проблема – видеопоток закодирован в формате mpeg2, а наша система поддерживает только OGG Theora. Исходного кода системы у нас нет, единственное что мы можем делать – менять датасет и обучать нейросеть. Что же делать? Написать класс-адаптер, который будет переводит поток из mpeg2 -> OGG Theora и отдавать в нейросеть.

По классической схеме в паттерне участвуют client, target, adaptee и adapter. Клиент в данном случае это нейросеть, получающая видеопоток в OGG Theora, таргет – интерфейс с которым она взаимодействует, adaptee – интерфейс отдающий видеопоток в mpeg2, адаптер – конвертирует mpeg2 в OGG Theora и отдает по интерфейсу target.

Вроде все просто?

Источники

https://ru.wikipedia.org/wiki/Адаптер_(шаблон_проектирования)
https://refactoring.guru/ru/design-patterns/adapter

Паттерн Делегат

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


// псевдокод

class BarbershopScreen {
   let calendar: Calendar

   func showBarbersList(date: Date) {
      showSelectionSheet(barbers(forDate: date))
   }
}

class Calendar {
    let screen: BarbershopScreen

    func handleTap(on date: Date) {
        screen.showBarbersList(date: date)
    }
}

Через несколько дней требования меняются, перед выводом списка нужно показывать предложения с выбором услуг (стрижка бороды и т.п.) но не всегда, во все дни кроме субботы.
Добавляем в календарь проверку суббота сегодня или нет, в зависимости от нее вызываем метод списка барберов или списка услуг, для наглядности продемонстрирую:


// псевдокод

class BarbershopScreen {
   let calendar: Calendar

   func showBarbersList(date: Date) {
      showSelectionSheet(barbers(forDate: date))
   }

   func showOffersList() {
      showSelectionSheet(offers)
   }
}

class Calendar {
    let screen: BarbershopScreen

    func handleTap(on date: Date)  {
        if date.day != .saturday {
             screen.showOffersList()
        }
        else {
             screen.showBarbersList(date: date)
        }
    }
}

Через неделю нас просят добавить календарь на экран обратной связи, и в этот момент случается первое архитектурное ой!
Что же делать? Календарь ведь связан намертво с экраном записи на стрижку.
ух! уф! ой-ой
Если продолжить работать с такой бредовой архитектурой приложения, то следует сделать копию всего класса календаря и связать эту копию с экраном обратной связи.
Ок вроде хорошо, далее мы добавили еще несколько экранов и несколько копий календаря, и тут наступил момент икс. Нас попросили изменить дизайн календаря, тоесть теперь нужно найти все копии календаря и добавить одинаковые изменения во все. Такой “подход” очень сказывается на скорости разработки, увеличивает шанс допустить ошибку. В итоге такие проекты оказываются в состоянии разбитого корыта, когда уже даже автор оригинальной архитектуры не понимает как работают копии его классов, прочие хаки добавленные по пути разваливаются на лету.
Что же нужно было делать, а лучше что еще не поздно начать делать? Использовать паттерн делегирования!
Делегирование это способ передавать события класса через общий интерфейс. Далее пример делегата для календаря:

protocol CalendarDelegate {
   func calendar(_ calendar: Calendar, didSelect date: Date)
}

Теперь добавим код работу с делегатом в код примера:


// псевдокод

class BarbershopScreen: CalendarDelegate {
   let calendar: Calendar

   init() {
       calendar.delegate = self
   }

   func calendar(_ calendar: Calendar, didSelect date: Date) {
        if date.day != .saturday {
            showOffersList()
        }
        else {
             showBarbersList(date: date)
        }
   }

   func showBarbersList(date: Date) {
      showSelectionSheet(barbers(forDate: date))
   }

   func showOffersList() {
      showSelectionSheet(offers)
   }
}

class Calendar {
    weak var delegate: CalendarDelegate

    func handleTap(on date: Date)  {
        delegate?.calendar(self, didSelect: date)
    }
}

В итоге мы отвязали календарь от экрана совсем, при выборе даты из календаря он передает событие выбора даты – *делегирует* обработку события подписчику; подписчиком выступает экран.
Какие преимущества мы получаем в таком подходе? Теперь мы можем менять календарь и логику экранов независимо друг от друга, не дублируя классы, упрощая дальнейшую поддержку; таким образом реализуется “принцип единственной ответственности” реализации компонентов системы, соблюдается принцип DRY.
При использовании делегирования можно добавлять, менять логику вывода окошек, очередности чего угодно на экране и это совершенно не будет затрагивать календарь и прочие классы, которые объективно и не должны участвовать в несвязанных напрямую с ними процессами.
Альтернативно, не особо утруждающие себя программисты, используют отправку сообщений через общую шину, без написания отдельного протокола/интерфейса делегата, там где лучше было бы использовать делегирование. О недостатках такого подхода я написал в прошлой заметке – “Паттерн Наблюдатель”.

Источники

https://refactoring.guru/ru/replace-inheritance-with-delegation

Паттерн Наблюдатель

Паттерн Наблюдатель (Observer) относится к поведенческим паттернам проектирования.
Паттерн позволяет отправлять изменение состояния объекта подписчикам, с использованием общего интерфейса.
Допустим мы разрабатываем мессенджер для программистов, у нас в приложении есть экран чата. При получении сообщения с текстом “проблема” и “ошибка” или “что-то не так”, нужно красить экран списка ошибок и экран настроек в красный цвет.
Далее я опишу 2 варианта решения задачи, первый простой но крайне сложный в поддержке, и второй гораздо стабильнее в поддержке, но требует включать голову при начальной реализации.

Общая шина

Все реализации паттерна содержат отправку сообщений при изменении данных, подписку на сообщения, дальнейшую обработку в методах. Вариант с общей шиной содержит единый объект (обычно используется синглтон) который обеспечивает диспетчеризацию сообщений получателям.
Простота реализации заключается в следующем:

  1. Объект отправляет абстрактное сообщение в общую шину
  2. Другой объект подписанный на общую шину, ловит сообщение и решает обработать его или нет.

Один из вариантов реализации доступный у Apple (подсистема NSNotificationCenter), добавлен матчинг заголовка сообщения на имя метода, который вызывается у получателя при доставке.
Самый большой минус такого подхода – при дальнейшем изменении сообщения нужно будет сначала вспомнить, а затем вручную отредактировать все места где оно обрабатывается, отправляется. Налицо случай быстрой первичной реализации, дальнейшей долгой, сложной поддержки, требующей базы знаний для корректной работы.

Мультикаст делегат

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

Источники

https://refactoring.gu/ru/design-patterns/observer

Паттерн Прокси

Паттерн Прокси относится к структурным паттернам проектирования.
Паттерн описывает технику работы с классом через класс прослойку – прокси. Прокси позволяет изменять функционал оригинального класса, с возможностью сохранения оригинального поведения, при сохранении оригинального интерфейса класса.
Представим ситуацию – в 2015 году одна из стран западной Европы решает записывать все запросы к сайтам пользователей страны, для улучшения статистики и углубленного понимания политических настроений граждан.
Представим псевдокод наивной имплементации шлюза которым пользуются граждане для выхода в интернет:

class InternetRouter {

    private let internet: Internet

    init(internet: Internet) {
        self.internet = internet
    }

    func handle(request: Request, from client: Client) -> Data {
        return self.internet.handle(request)
    }

}

В приведенном выше коде, мы создаем класс интернет-роутера, с указателем на объект предоставляющий доступ в интернет. При обращении клиента с запросом сайта, мы возвращает ответ из интернета.

Используя паттерн Прокси и антипаттерн синглтон, добавим функционал логирования имени клиента и URL:

class InternetRouterProxy {

    private let internetRouter: InternetRouter

    init(internet: Internet) {
        self.internetRouter = InternetRouter(internet: internet)
    }

    func handle(request: Request, from client: Client) -> Data {

        Logger.shared.log(“Client name: \(client.name), requested URL: \(request.URL)”)

        return self.internetRouter.handle(request: request, from: client)
    }

}

Из-за сохранения оригинального интерфейса InternetRouter в классе прокси InternetRouterProxy, достаточно заменить класс инициализации с InternerRouter на его прокси, больше изменений в кодовой базе не потребуется.

Источники

https://refactoring.guru/ru/design-patterns/proxy

Паттерн Прототип

Паттерн прототип относится к группе порождающих шаблонов проектирования.
Допустим мы разрабатываем приложения для знакомств Tender, по бизнес-модели у нас предусмотрена платная возможность делать копии собственного профиля меняя имя автоматически, и порядок фотографий местами. Сделано это для того чтобы пользователь мог иметь возможность вести сразу несколько профилей с разным набором друзей в приложении.
По нажатию на кнопку создания копии профиля, нам нужно реализовать копирование профиля, авто генерацию имени и пересортировку фотографий.
Наивная имплементация псевдокодом:

fun didPressOnCopyProfileButton() {
    let profileCopy = new Profile()
    profileCopy.name = generateRandomName()
    profileCopy.age = profile.age
    profileCopy.photos = profile.photos.randomize()
    storage.save(profileCopy)
}

Теперь представим что другие члены команды сделали copy-paste кода копирования или придумали его с нуля, и после этого добавилось новое поле – likes. В данном поле хранится количество лайков профиля, теперь нужно обновить *все* места где происходит копирование вручную, добавив новое поле. Это очень долго и сложно в поддержке кода, как и в тестировании.
Для решения этой проблемы придуман паттерн проектирования Прототип. Создадим общий протокол Copying, с методом copy() который возвращает копию объекта с необходимыми полями. После изменения полей сущности нужно будет обновить только один метод copy(), вместо того чтобы искать и обновлять вручную все места содержащие код копирования.

Источники

https://refactoring.guru/ru/design-patterns/prototype

Стейт машина и паттерн Состояние

В данной заметке я опишу использование стейт машины (Машину состояний), покажу простую реализацию, реализацию с использованием паттерна Состояние. Стоит оговориться, что использовать паттерн Состояние нежелательно при наличии меньше трех состояний, т.к. обычно это приводит к излишнему усложнению читаемости кода, сопутствующими проблемам в поддержке – все должно быть в меру.

MEAACT PHOTO / STUART PRICE.

Повелитель флажков

Допустим мы разрабатываем экран видеоплеер для медиасистемы гражданского самолета, плеер должен уметь загружать видео поток, проигрывать его, позволять пользователю останавливать процесс загрузки, перематывать и производить прочие операции обыкновенные для плеера.
Допустим плеер закэшировал очередной чанк видеопотока, проверил что чанков достаточно для воспроизведения, начал воспроизводить пользователю фрагмент и одновременно продолжает загрузку следующего.
В этот момент пользователь перематывает в середину ролика, тоесть теперь нужно остановить проигрывание текущего фрагмента, и начать загрузку с новой позиции. Однако существуют ситуации в которых такое делать нельзя – пользователь не может управлять проигрыванием видеопотока, пока ему показывают видеоролик о безопасности в воздухе. Заведем флажок isSafetyVideoPlaying для проверки данной ситуации.
Также система должна уметь ставить на паузу текущее видео и транслировать оповещение от капитана судна и экипажа через плеер. Заведем еще один флажок isAnnouncementPlaying. Плюс ко всему появилось требование не приостанавливать воспроизведение во время вывода справки по работе с плеером, еще один флажок isHelpPresenting.

Псевдокод примера медиаплеера:

class MediaPlayer {

    public var isHelpPresenting = false
    public var isCaching = false
    public var isMediaPlaying: Bool = false
    public var isAnnouncementPlaying = false
    public var isSafetyVideoPlaying = false

    public var currentMedia: Media = null

    fun play(media: Media) {

        if isMediaPlaying == false, isAnnouncementPlaying == false, isSafetyVideoPlaying == false {

            if isCaching == false {
                if isHelpPresenting == false {
                    media.playAfterHelpClosed()
                }
                else {
                    media.playAfterCaching()
                }
            }
    }

    fun pause() {
        if isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
            currentMedia.pause()
        }
    }
}

Приведенный выше пример плохо читаем, такой код сложно поддерживать из-за большой вариативности (энтропии) Данный пример основывается на моем опыте работы с кодовой базой *многих* проектов, где не использовалась стейт-машина.
Каждый флажок должен по особенному “управлять” элементами интерфейса, бизнес-логики приложения, разработчик, добавляя очередной флажок, должен уметь жонглировать ими, проверяя и перепроверяя все по нескольку раз со всеми возможными вариантами.
Подставляем в формулу “2 ^ количество флажков” можно получить 2 ^ 6 = 64 варианта поведения приложения для всего 6 флажков, все эти сочетания флажков нужно будет проверять и поддерживать вручную.
Со стороны разработчика добавление нового функционала при такой системе выглядит так:
– Нужно добавить возможность показывать страницу браузера авиакомпании, при этом она должна сворачиваться как с фильмами, если члены экипажа что-то объявляют.
– Ок, сделаю. (Ох чёрт придется добавить еще один флаг, и перепроверить все места где пересекаются флажки, это же сколько всего надо поменять!)

Также слабое место флажковой системы – внесение изменений в поведение приложения. Очень сложно представить как быстро/гибко менять поведение основанное на флажках, если после изменения одного только флага приходится все перепроверять. Такой подход к разработке приводит к большому количеству проблем, потере времени и денег.

Enter The Machine

Если хорошо присмотреться к флажкам, то можно понять что на самом деле мы пытаемся обработать конкретные процессы происходящие в реальном мире. Перечислим их: обычный режим, показ видео безопасности, трансляция сообщения от капитана или членов экипажа. Для каждого процесса известен свод правил, которые меняют поведение приложения.
По правилам паттерна стейт машины (машины состояний), перечислим все процессы как состояния в enum, добавим такое понятие как стейт в код плеера, реализуем поведение на основе стейта, удалив комбинации на флажках. Таким образом мы снизим варианты для тестирования ровно до количества состояний.

Псевдокод:

enum MediaPlayerState {
	mediaPlaying,
	mediaCaching,
	crewSpeaking,
	safetyVideoPlaying,
	presentingHelp
}

class MediaPlayer {
	fun play(media: Media) {
		media.play()
	}

	func pause() {
		media.pause()
	}
}

class MediaPlayerStateMachine {
	public state: MediaPlayerState
	public mediaPlayer: MediaPlayer
	public currentMedia: Media

	//.. init (mediaPlayer) etc

	public fun set(state: MediaPlayerState) {
		switch state {
			case mediaPlaying:
				mediaPlayer.play(currentMedia)
			case mediaCaching, crewSpeaking,
			safetyVideoPlaying, presentingHelp:
				mediaPlayer.pause()
		}
	}
}

Огромная разница между флажковой системой и стейт-машиной заключается в воронке переключения логического состояния в методе set(state: ..), оно позволяет транслировать человеческое понимание состояния в программный код, без необходимости играть в логические игры по конвертации флажков в состояния при дальнейшей поддержке кода.

Паттерн Состояние

Далее я покажу разницу между наивной имплементацией стейт машины и паттерном состояние. Представим что понадобилось добавить 10 стейтов, в итоге класс стейт-машины разрастется до размеров godobject, его будет сложно и затратно поддерживать. Конечно данная реализация лучше флажковой, (при флажковой системе застрелится сначала разработчик, а если и нет, то увидев 2 ^ 10 = 1024 вариаций повесится QA, однако если они оба *не заметят* сложности задачи, то ее заметит пользователь, у которого приложение просто откажется работать при определенном сочетании флажков)
При большом количестве состояний необходимо использовать паттерн Состояния.
Вынесем свод правил в протокол Состояния:

protocol State {
    func playMedia(media: Media, context: MediaPlayerContext)
    func shouldCacheMedia(context: MediaPlayerContext)
    func crewSpeaking(context: MediaPlayerContext)
    func safetyVideoPlaying(context:MediaPlayerContext)
    func presentHelp(context: MediaPlayerContext)
}

Вынесем реализацию свода правил в отдельные состояния, для примера код одного состояния:

class CrewSpeakingState: State {
	func playMedia(context: MediaPlayerContext) {
		showWarning(“Can’ t play media - listen to announce!”)
	}

	func mediaCaching(context: MediaPlayerContext) {
		showActivityIndicator()
	}

	func crewSpeaking(context: MediaPlayerContext) {
		set(volume: 100)
	}

	func safetyVideoPlaying(context: MediaPlayerContext) {
		set(volume: 100)
	}

	func presentHelp(context: MediaPlayerContext) {
		showWarning(“Can’ t present help - listen to announce!”)
	}
}

Далее создадим контекст с которым будет работать каждый стейт, интегрируем стейт-машину:

final class MediaPlayerContext {
	private
	var state: State

	public fun set(state: State) {
		self.state = state
	}

	public fun play(media: Media) {
		state.play(media: media, context: this)
	}

	…
	остальные возможные события
}

Компоненты приложения работают с контекстом через публичные методы, объекты стейты сами решают из какого стейта в какой осуществлять переход с помощью стейт-машины внутри контекста.
Таким образом мы реализовали декомпозицию God Object, поддерживать изменяющееся состояние будет гораздо проще, благодаря отслеживанию изменений в протоколе компилятором, уменьшения сложности понимания состояний из-за уменьшения количества строк кода и фокусе на решении конкретной задачи состояния. Также теперь можно разделять работу в команде, отдавая реализацию конкретного состояния членам команды, не беспокоясь о необходимости “разруливать” конфликты, что происходит при работе с одним большим классом стейт-машины.

Источники

https://refactoring.guru/ru/design-patterns/state

Шаблонный метод

Шаблонный метод относится к поведенческим шаблонам проектирования. Паттерн описывает способ замены части логики класса по требованию, оставляя общую часть неизменяемой для потомков.

Cuban Cars

Допустим мы разрабатываем банк-клиент, рассмотрим задачу разработки модуля авторизации – пользователь должен иметь возможность авторизоваться в приложении используя абстрактные данные для входа.
Модуль авторизации должен быть кроссплатформенным, поддерживать разные технологии авторизации и хранения зашифрованных данных разных платформ. Для реализации модуля мы выбираем кроссплатформенный язык Kotlin, используя абстрактный класс (протокол) модуля авторизации, напишем реализацию для телефона MyPhone:

class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
    fun loginAndPassword() : Pair {
        return Pair("admin", "qwerty65435")
    }
}

class ServerApiClient {
    fun authorize(authorizationData: AuthorizationData) : Unit {
        println(authorizationData.login)
        println(authorizationData.password)
        println("Authorized")
    }
}

class AuthorizationData {
    var login: String? = null
    var password: String? = null
}

interface AuthorizationModule {
    abstract fun fetchAuthorizationData() : AuthorizationData
    abstract fun authorize(authorizationData: AuthorizationData)
}

class MyPhoneAuthorizationModule: AuthorizationModule {
    
    override fun fetchAuthorizationData() : AuthorizationData {
        val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
        val authorizationData = AuthorizationData()
        authorizationData.login = loginAndPassword.first
        authorizationData.password = loginAndPassword.second
        
        return authorizationData
    }
    
    override fun authorize(authorizationData: AuthorizationData) {
        ServerApiClient().authorize(authorizationData)
    }
    
}

fun main() {
    val authorizationModule = MyPhoneAuthorizationModule()
    val authorizationData = authorizationModule.fetchAuthorizationData()
    authorizationModule.authorize(authorizationData)
}

Теперь для каждого телефона/платформы нам придется дублировать код отправки авторизации на сервер, налицо нарушение принципа DRY. Приведенный выше пример очень прост, в более комплексных классах дублирования будет еще больше. Для устранения дублирования кода следует использовать паттерн Шаблонный метод.
Вынесем общие части модуля в неизменяемые методы, функционал передачи зашифрованных данных переложим на конкретные классы платформ:

class MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage {
    fun loginAndPassword() : Pair {
        return Pair("admin", "qwerty65435")
    }
}

class ServerApiClient {
    fun authorize(authorizationData: AuthorizationData) : Unit {
        println(authorizationData.login)
        println(authorizationData.password)
        println("Authorized")
    }
}

class AuthorizationData {
    var login: String? = null
    var password: String? = null
}

interface AuthorizationModule {
    abstract fun fetchAuthorizationData() : AuthorizationData
    
    fun authorize(authorizationData: AuthorizationData) {
        ServerApiClient().authorize(authorizationData)
    }
}

class MyPhoneAuthorizationModule: AuthorizationModule {
    
    override fun fetchAuthorizationData() : AuthorizationData {
        val loginAndPassword = MyPhoneSuperDuperSecretMyPhoneAuthorizationStorage().loginAndPassword()
        val authorizationData = AuthorizationData()
        authorizationData.login = loginAndPassword.first
        authorizationData.password = loginAndPassword.second
        
        return authorizationData
    }
    
}

fun main() {
    val authorizationModule = MyPhoneAuthorizationModule()
    val authorizationData = authorizationModule.fetchAuthorizationData()
    authorizationModule.authorize(authorizationData)
}

Источники

https://refactoring.guru/ru/design-patterns/template-method

Исходный код

https://gitlab.com/demensdeum/patterns/

Паттерн Мост

Паттерн Мост относится к структурным паттернам проектирования. Он позволяет абстрагировать реализацию логики класса, с помощью переноса логики в отдельный абстрактный класс. Звучит просто, да?

Допустим мы реализуем спам-бота который должен уметь отправлять сообщения в разные типы мессенджеров.
Реализуем с помощью общего протокола:

protocol User {
    let token: String
    let username: String
}

protocol Messenger {
    var authorize(login: String, password: String)
    var send(message: String, to user: User)
}

class iSeekUUser: User {
    let token: String
    let username: String
}

class iSeekU: Messenger {

    var authorizedUser: User?
    var requestSender: RequestSender?
    var requestFactory: RequestFactory?

    func authorize(login: String, password: String) {
        authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
    }
    
    func send(message: String, to user: User) {
        requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
    }
}

class SpamBot {
    func start(usersList: [User]) {
        let iSeekUMessenger = iSeekU()
        iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
        
        for user in usersList {
            iSeekUMessennger.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
        }
    }
}

Теперь представим ситуацию выхода нового более быстрого протокола отправки сообщений для мессенджера iSekU. Для добавления нового протокола нужно будет продублировать имплементацию бота iSekU, изменив лишь малую его часть. Непонятно зачем это делать если изменилась лишь малая часть логики класса. При таком подходе нарушается принцип DRY, при дальнейшем развитии продукта, отсутствие гибкости даст о себе знать ошибками и задержками в имплементации новых возможностей.
Вынесем логику работы протокола в абстрактный класс, таким образом реализовав паттерн Мост:

protocol User {
    let token: String
    let username: String
}

protocol Messenger {
    var authorize(login: String, password: String)
    var send(message: String, to user: User)
}

protocol MessagesSender {
    func send(message: String, to user: User)
}

class iSeekUUser: User {
    let token: String
    let username: String
}

class iSeekUFastMessengerSender: MessagesSender {
    func send(message: String, to user: User) {
        requestSender?.perform(requestFactory.messageRequest(message: message, to: user)
    }
}

class iSeekU: Messenger {

    var authorizedUser: User?
    var requestSender: RequestSender?
    var requestFactory: RequestFactory?
    var messagesSender: MessengerMessagesSender?

    func authorize(login: String, password: String) {
        authorizedUser = requestSender?.perform(requestFactory.loginRequest(login: login, password: password))
    }
    
    func send(message: String, to user: User) {
        messagesSender?.send(message: message, to: user)
    }
}

class SpamBot {

    var messagesSender: MessagesSender?

    func start(usersList: [User]) {
        let iSeekUMessenger = iSeekU()
        iSeekUMessenger.authorize(login: "SpamBot", password: "SpamPassword")
        
        for user in usersList {
            messagesSender.send(message: "Hey checkout demensdeum blog! http://demensdeum.com", to: user)
        }
    }
}

Одним из плюсов такого подхода несомненно является возможность расширять функционал приложения, с помощью написания плагинов/библиотек, реализующих абстрагированную логику, не изменяя при этом кода основного приложения.
В чем разница с паттерном Стратегия? Оба паттерны очень похожи, однако Стратегия описывает переключения *алгоритмов*, в то время как Мост позволяет переключать большие части *любой сколь сложной логики*.

Источники

https://refactoring.guru/ru/design-patterns/bridge

Исходный код

https://gitlab.com/demensdeum/patterns/

Паттерн Цепочка Обязанностей

Цепочка обязанностей относится к поведенческим шаблонам проектирования.


Ганна Долбієва

Кинокомпания Джа-пикчерс сняла фильм документальный фильм про коммунистов-растаманов из Либерии под названием “Красная заря Марли”. Фильм очень долгий (8 часов), интересный, однако перед отправкой в прокат оказалось что в некоторых странах кадры и фразы из фильмы могут счесть ересью и не дать прокатной лицензии. Продюсеры киноленты решают вырезать моменты содержащие сомнительные фразы из фильма, в ручном и в автоматическом режиме. Двойная проверка нужна для того чтобы представителей прокатчика банально не расстреляли в некоторых странах, в случае ошибки при ручном отсмотре и монтаже.
Страны делятся на четыре группы – страны без цензуры, с умеренной, средней и очень строгой цензурой. Принимается решение использовать нейросети для классификации уровня ереси в отсматриевом фрагменте фильма. Для проекта закупаются очень дорогие state-of-art нейронки обученные на разные уровни цензуры, задача разработчика – разбить фильм на фрагменты и передавать их по цепочке нейросетей, от вольной до строгой, пока одна из них не обнаружит ересь, дальше фрагмент передается на ручной отсмотр для дальнейшего монтажа. Делать проход по всем нейронкам нельзя, т.к. на их работу затрачивается слишком много вычислительных мощностей (нам ведь еще за свет платить) достаточно остановиться на первой сработавшей.
Наивная имлементация псевдокодом:

import StateOfArtCensorshipHLNNClassifiers

protocol MovieCensorshipClassifier {
    func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}

class CensorshipClassifier: MovieCensorshipClassifier {

    let hnnclassifier: StateOfArtCensorshipHLNNClassifier

    init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier) {
        self.hnnclassifier = hnnclassifier
    }
    
    func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
        return hnnclassifier.shouldBeCensored(movieChunk)
    }
}

let lightCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"))
let normalCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal"))
let hardCensorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard"))

let classifiers = [lightCensorshipClassifier, normalCensorshipClassifier, hardCensorshipClassifier]

let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
    for classifier in classifiers {
        if classifier.shouldBeCensored(chunk) == true {
            print("Should censor movie chunk: \(chunk), reported by \(classifier)")
        }
   }
}

Вообщем и целом решение с массивом классификаторов не такое уж и плохое, Однако! представим что мы не можем создавать массив, нам доступна возможность создавать только одну сущность классификатора, который уже определяет тип цензуры для фрагмента фильма. Такие ограничения возможны при разработки библиотеки расширяющей функционал приложения (плагин).
Воспользуемся паттерном декоратор – добавим в класс классификатора референс на следующий классификатор в цепочке, будем останавливать процесс проверки на первой успешной классификации.
Таким образом мы реализуем паттерн Цепочка Обязанности:

import StateOfArtCensorshipHLNNClassifiers

protocol MovieCensorshipClassifier {
    func shouldBeCensored(movieChunk: MovieChunk) -> Bool
}

class CensorshipClassifier: MovieCensorshipClassifier {

    let nextClassifier: CensorshipClassifier?
    let hnnclassifier: StateOfArtCensorshipHLNNClassifier

    init(_ hnnclassifier: StateOfArtCensorshipHLNNClassifier, nextClassifier: CensorshipClassifiers?) {
            self.nextClassifier = nextClassifier
            self.hnnclassifier = hnnclassifier
    }
    
    func shouldBeCensored(_ movieChunk: MovieChunk) -> Bool {
        let result = hnnclassifier.shouldBeCensored(movieChunk)
        
        print("Should censor movie chunk: \(movieChunk), reported by \(self)")
        
        if result == true {
                return true
        }
        else {
                return nextClassifier?.shouldBeCensored(movieChunk) ?? false
        }
    }
}

let censorshipClassifier = CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("light"), nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("normal", nextClassifier: CensorshipClassifier(StateOfArtCensorshipHLNNClassifier("hard")))))

let movie = Movie("Red Jah rising")
for chunk in movie.chunks {
    censorshipClassifier.shouldBeCensored(chunk)
}

References

https://refactoring.guru/ru/design-patterns/chain-of-responsibility

Source Code

https://gitlab.com/demensdeum/patterns/

Паттерн Декоратор

Паттерн Декоратор относится к структурным паттернам проектирования.

Декоратор используется как альтернатива наследованию для расширения функционала классов.
Имеется задача расширения функционала приложения в зависимости от типа продукта. Заказчику необходимы три типа продукта – Basic, Professional, Ultimate.
Basic – считает количество символов, Professional – возможности Basic + печатает текст большими буквами, Ultimate – Basic + Professional + печатает текст с надписью ULTIMATE.
Реализуем с помощью наследования:

protocol Feature {
	func textOperation(text: String)
}

class BasicVersionFeature: Feature {
	func textOperation(text: String) {
		print("\(text.count)")
	}
}

class ProfessionalVersionFeature: BasicVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.uppercased())")
	}
}

class UltimateVersionFeature: ProfessionalVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")
	}
}

let textToFormat = "Hello Decorator"

let basicProduct = BasicVersionFeature()
basicProduct.textOperation(text: textToFormat)

let professionalProduct = ProfessionalVersionFeature()
professionalProduct.textOperation(text: textToFormat)

let ultimateProduct = UltimateVersionFeature()
ultimateProduct.textOperation(text: textToFormat)

Теперь появляется требование реализовать продукт “Ultimate Light” – Basic + Ultimate но без возможностей Professional версии. Случается первый ОЙ!, т.к. придется создавать отдельный класс для такой простой задачи, дублировать код.
Продолжим реализацию с помощью наследования:

protocol Feature {
	func textOperation(text: String)
}

class BasicVersionFeature: Feature {
	func textOperation(text: String) {
		print("\(text.count)")
	}
}

class ProfessionalVersionFeature: BasicVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.uppercased())")
	}
}

class UltimateVersionFeature: ProfessionalVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")
	}
}

class UltimateLightVersionFeature: BasicVersionFeature {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")	
	}
}

let textToFormat = "Hello Decorator"

let basicProduct = BasicVersionFeature()
basicProduct.textOperation(text: textToFormat)

let professionalProduct = ProfessionalVersionFeature()
professionalProduct.textOperation(text: textToFormat)

let ultimateProduct = UltimateVersionFeature()
ultimateProduct.textOperation(text: textToFormat)

let ultimateLightProduct = UltimateLightVersionFeature()
ultimateLightProduct.textOperation(text: textToFormat)

Пример можно развивать для наглядности и дальше, но уже и сейчас видна сложность поддержки системы основанной на базе наследования – громоздкость и отсутствие гибкости.
Декоратор представляет из себя совокупность протокола описывающего функционал, абстрактного класса содержащего референс на дочерний конкретный экземпляр класса декоратора который расширяет функционал.
Перепишем пример выше с использованием паттерна:

protocol Feature {
	func textOperation(text: String)
}

class FeatureDecorator: Feature {
	private var feature: Feature?
	
	init(feature: Feature? = nil) {
		self.feature = feature
	}
	
	func textOperation(text: String) {
		feature?.textOperation(text: text)
	}
}

class BasicVersionFeature: FeatureDecorator {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.count)")
	}
}

class ProfessionalVersionFeature: FeatureDecorator {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("\(text.uppercased())")
	}
}

class UltimateVersionFeature: FeatureDecorator {
	override func textOperation(text: String) {
		super.textOperation(text: text)
		print("ULTIMATE: \(text)")
	}
}

let textToFormat = "Hello Decorator"

let basicProduct = BasicVersionFeature(feature: UltimateVersionFeature())
basicProduct.textOperation(text: textToFormat)

let professionalProduct = ProfessionalVersionFeature(feature: UltimateVersionFeature())
professionalProduct.textOperation(text: textToFormat)

let ultimateProduct = BasicVersionFeature(feature: UltimateVersionFeature(feature: ProfessionalVersionFeature()))
ultimateProduct.textOperation(text: textToFormat)

let ultimateLightProduct = BasicVersionFeature(feature: UltimateVersionFeature())
ultimateLightProduct.textOperation(text: textToFormat)

Теперь мы можем создавать вариации продукта любого типа – достаточно инициализировать комбинированные типы на этапе запуска приложения, пример ниже представляет из себя создание Ultimate + Professional версии:

ultimateProfessionalProduct.textOperation(text: textToFormat)

Источники

https://refactoring.guru/ru/design-patterns/decorator

Исходный код

https://gitlab.com/demensdeum/patterns

Паттерн Медиатор

Паттерн Медиатор относится к поведенческим паттернам проектирования.

Однажды вам поступает заказ разработать приложение-шутку – пользователь нажимает на кнопку посредине экрана и раздается смешной звук кряканья утки.
После выгрузки в аппстор, приложение становится хитом: все крякают через ваше приложение, Илон Маск крякает в своем инстаграме на очередном запуске сверх-скоростного тоннеля на марсе, Хиллари Клинтон перекрякивает Дональда Трампа на дебатах и выигрывает выборы на Украине, успех!
Наивная имплементация приложения выглядит так:

class DuckButton {
    func didPress() {
        print("quack!")
    }
}

let duckButton = DuckButton()
duckButton.didPress()

Далее вы решаете добавить звук гавканья собачки, для этого вам надо показать две кнопки для выбора звука – с уточкой и собачкой. Создаем два класса кнопок DuckButton и DogButton.
Меняем код:

class DuckButton {
    func didPress() {
        print("quack!")
    }
}

class DogButton {
    func didPress() {
        print("bark!")
    }
}

let duckButton = DuckButton()
duckButton.didPress()

let dogButton = DogButton()
dogButton.didPress()

После очередного успеха добавляем звук свинячьего визга, уже три класса кнопок:

class DuckButton {
    func didPress() {
        print("quack!")
    }
}

class DogButton {
    func didPress() {
        print("bark!")
    }
}

class PigButton {
    func didPress() {
        print("oink!")
    }
}

let duckButton = DuckButton()
duckButton.didPress()

let dogButton = DogButton()
dogButton.didPress()

let pigButton = PigButton()
pigButton.didPress()

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

class DuckButton {
    var isMakingSound = false
    var dogButton: DogButton?
    var pigButton: PigButton?
    func didPress() {
        guard dogButton?.isMakingSound ?? false == false &&
                pigButton?.isMakingSound ?? false == false else { return }
        isMakingSound = true
        print("quack!")
        isMakingSound = false
    }
}

class DogButton {
    var isMakingSound = false
    var duckButton: DuckButton?
    var pigButton: PigButton?
    func didPress() {
        guard duckButton?.isMakingSound ?? false == false &&
                pigButton?.isMakingSound ?? false == false else { return }
        isMakingSound = true
        print("bark!")
        isMakingSound = false
    }
}

class PigButton {
    var isMakingSound = false
    var duckButton: DuckButton?
    var dogButton: DogButton?
    func didPress() {
        guard duckButton?.isMakingSound ?? false == false && 
                dogButton?.isMakingSound ?? false == false else { return }
        isMakingSound = true
        print("oink!")
        isMakingSound = false
    }
}

let duckButton = DuckButton()
duckButton.didPress()

let dogButton = DogButton()
dogButton.didPress()

let pigButton = PigButton()
pigButton.didPress()

На волне успеха вашего приложения, правительство решает сделать закон по которому крякать, гавкать и хрюкать на мобильных устройствах можно только с 9:00 утра и до 15:00 в будние дни, в остальное время пользователь вашего приложения рискует сесть в тюрьму на 5 лет за непристойное звукоизвлечение с использованием электронных средств личного пользования.
Меняем код:

import Foundation

extension Date {
    func mobileDeviceAllowedSoundTime() -> Bool {
        let hour = Calendar.current.component(.hour, from: self)
        let weekend = Calendar.current.isDateInWeekend(self)
        
        let result = hour >= 9 && hour <= 14 && weekend == false
        
        return result
    }
}

class DuckButton {
    var isMakingSound = false
    var dogButton: DogButton?
    var pigButton: PigButton?
    func didPress() {
        guard dogButton?.isMakingSound ?? false == false &&
                pigButton?.isMakingSound ?? false == false &&
                 Date().mobileDeviceAllowedSoundTime() == true else { return }
        isMakingSound = true
        print("quack!")
        isMakingSound = false
    }
}

class DogButton {
    var isMakingSound = false
    var duckButton: DuckButton?
    var pigButton: PigButton?
    func didPress() {
        guard duckButton?.isMakingSound ?? false == false &&
                pigButton?.isMakingSound ?? false == false &&
                 Date().mobileDeviceAllowedSoundTime() == true else { return }
        isMakingSound = true
        print("bark!")
        isMakingSound = false
    }
}

class PigButton {
    var isMakingSound = false
    var duckButton: DuckButton?
    var dogButton: DogButton?
    func didPress() {
        guard duckButton?.isMakingSound ?? false == false && 
                dogButton?.isMakingSound ?? false == false &&
                 Date().mobileDeviceAllowedSoundTime() == true else { return }
        isMakingSound = true
        print("oink!")
        isMakingSound = false
    }
}

let duckButton = DuckButton()
let dogButton = DogButton()
let pigButton = PigButton()

duckButton.dogButton = dogButton
duckButton.pigButton = pigButton

dogButton.duckButton = duckButton
dogButton.pigButton = pigButton

pigButton.duckButton = duckButton
pigButton.dogButton = dogButton

duckButton.didPress()
dogButton.didPress()
pigButton.didPress()

Внезапно приложение-фонарик начинает вытеснять наше с рынка, не дадим ему победить нас и добавляем фонарик по нажатию на кнопку “хрю-хрю”, и остальным кнопкам со-но:

import Foundation

extension Date {
    func mobileDeviceAllowedSoundTime() -> Bool {
        let hour = Calendar.current.component(.hour, from: self)
        let weekend = Calendar.current.isDateInWeekend(self)
        
        let result = hour >= 9 && hour <= 14 && weekend == false
        
        return result
    }
}

class Flashlight {

    var isOn = false

    func turn(on: Bool) {
        isOn = on
    }
}

class DuckButton {
    var isMakingSound = false
    var dogButton: DogButton?
    var pigButton: PigButton?
    var flashlight: Flashlight?
    func didPress() {
        flashlight?.turn(on: true)
        guard dogButton?.isMakingSound ?? false == false &&
                pigButton?.isMakingSound ?? false == false &&
                 Date().mobileDeviceAllowedSoundTime() == true else { return }
        isMakingSound = true
        print("quack!")
        isMakingSound = false
    }
}

class DogButton {
    var isMakingSound = false
    var duckButton: DuckButton?
    var pigButton: PigButton?
    var flashlight: Flashlight?
    func didPress() {
        flashlight?.turn(on: true)
        guard duckButton?.isMakingSound ?? false == false &&
                pigButton?.isMakingSound ?? false == false &&
                 Date().mobileDeviceAllowedSoundTime() == true else { return }
        isMakingSound = true
        print("bark!")
        isMakingSound = false
    }
}

class PigButton {
    var isMakingSound = false
    var duckButton: DuckButton?
    var dogButton: DogButton?
    var flashlight: Flashlight?
    func didPress() {
        flashlight?.turn(on: true)
        guard duckButton?.isMakingSound ?? false == false && 
                dogButton?.isMakingSound ?? false == false &&
                 Date().mobileDeviceAllowedSoundTime() == true else { return }
        isMakingSound = true
        print("oink!")
        isMakingSound = false
    }
}

let flashlight = Flashlight()
let duckButton = DuckButton()
let dogButton = DogButton()
let pigButton = PigButton()

duckButton.dogButton = dogButton
duckButton.pigButton = pigButton
duckButton.flashlight = flashlight

dogButton.duckButton = duckButton
dogButton.pigButton = pigButton
dogButton.flashlight = flashlight

pigButton.duckButton = duckButton
pigButton.dogButton = dogButton
pigButton.flashlight = flashlight

duckButton.didPress()
dogButton.didPress()
pigButton.didPress()

В итоге мы имеем огромное приложение которое содержит много copy-paste кода, классы внутри связаны друг с другом мертвой связкой - отсутствует слабая связанность, такое чудо очень сложно поддерживать и изменять в дальнейшем из-за высоких шансов допустить ошибку.

Используем Медиатор

Добавим промежуточный класс медиатор - ApplicationController. Данный класс будет обеспечивать слабую связанность объектов, обеспечивает разделение ответственности классов, позволит устранить дублирующий код.
Перепишем:

import Foundation

class ApplicationController {

    private var isMakingSound = false
    private let flashlight = Flashlight()
    private var soundButtons: [SoundButton] = []

    func add(soundButton: SoundButton) {
        soundButtons.append(soundButton)
    }
    
    func didPress(soundButton: SoundButton) {
        flashlight.turn(on: true)
        guard Date().mobileDeviceAllowedSoundTime() && 
                isMakingSound == false else { return }
        isMakingSound = true
        soundButton.didPress()
        isMakingSound = false
    }
}

class SoundButton {
    let soundText: String
    
    init(soundText: String) {
        self.soundText = soundText
    }
    
    func didPress() {
        print(soundText)
    }
}

class Flashlight {
    var isOn = false

    func turn(on: Bool) {
        isOn = on
    }
}

extension Date {
    func mobileDeviceAllowedSoundTime() -> Bool {
        let hour = Calendar.current.component(.hour, from: self)
        let weekend = Calendar.current.isDateInWeekend(self)
        
        let result = hour >= 9 && hour <= 14 && weekend == false
        
        return result
    }
}

let applicationController = ApplicationController()
let pigButton = SoundButton(soundText: "oink!")
let dogButton = SoundButton(soundText: "bark!")
let duckButton = SoundButton(soundText: "quack!")

applicationController.add(soundButton: pigButton)
applicationController.add(soundButton: dogButton)
applicationController.add(soundButton: duckButton)

pigButton.didPress()
dogButton.didPress()
duckButton.didPress()

Во многих статьях посвященных архитектурам приложений с пользовательским интерфейсом описывается паттерн MVC и производные. Модель используется для работы с данными бизнес-логики, view или представление показывает информацию пользователю в интерфейсе/обеспечивает взаимодействие с пользователем, контроллер является медиатором обеспечивающим взаимодействие компонентов системы.

Источники

https://refactoring.guru/ru/design-patterns/mediator

Исходный код

https://gitlab.com/demensdeum/patterns/

Паттерн Стратегия

Паттерн “Стратегия” позволяет выбирать тип алгоритма, который реализует общий интерфейс, прямо во время работы приложения.
Данный паттерн относится к поведенческим шаблонам проектирования.

Сунь Цзы

Допустим мы разрабатываем музыкальный плеер со встроенными кодеками. Под встроенными кодеками подразумеваяется чтение музыкальных форматов без использования внешних источников операционной системы (кодеков), плеер должен уметь читать треки разных форматов и воспроизводить их. Такими возможностями обладает плеер VLC, он поддерживает разные типы видео и аудио форматов, запускается на популярных и не очень операционных системах.

Представим как выглядит наивная имплементация плеера:

var player: MusicPlayer?

func play(filePath: String) {
    let extension = filePath.pathExtension

    if extension == "mp3" {
        playMp3(filePath)
    }
    else if extension == "ogg" {
        playOgg(filePath)
    }
}

func playMp3(_ filePath: String) {
    player = MpegPlayer()
    player?.playMp3(filePath)
}

func playOgg(_ filePath: String) {
    player = VorbisPlayer()
    player?.playMusic(filePath)
}

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

Создадим общий протокол MusicPlayerCodecAlgorithm, напишем реализацию протокола в двух классах MpegMusicPlayerCodecAlgorithm и VorbisMusicPlayerCodecAlgorithm, для проигрывания mp3 и ogg файлов со-но. Создадим класс MusicPlayer, который будет содержать референс на алгоритм который необходимо переключать, далее по расширению файла реализуем переключение типа кодека:

import Foundation

class MusicPlayer {
    var playerCodecAlgorithm: MusicPlayerCodecAlgorithm?
    
	func play(_ filePath: String) {
            playerCodecAlgorithm?.play(filePath)
	}
}

protocol MusicPlayerCodecAlgorithm {
    func play(_ filePath: String)
}

class MpegMusicPlayerCodecAlgorithm: MusicPlayerCodecAlgorithm {
	func play(_ filePath: String) {
		debugPrint("mpeg codec - play")
	}
}

class VorbisMusicPlayerCodecAlgorithm: MusicPlayerCodecAlgorithm {
	func play(_ filePath: String) {
		debugPrint("vorbis codec - play")	
	}
}

func play(fileAtPath path: String) {
	guard let url = URL(string: path) else { return }
	let fileExtension = url.pathExtension
		
	let musicPlayer = MusicPlayer()
	var playerCodecAlgorithm: MusicPlayerCodecAlgorithm? 
		
	if fileExtension == "mp3" {
                playerCodecAlgorithm = MpegMusicPlayerCodecAlgorithm()
	}
	else if fileExtension == "ogg" {
                playerCodecAlgorithm = VorbisMusicPlayerCodecAlgorithm()
	}
		
	musicPlayer.playerCodecAlgorithm = playerCodecAlgorithm
	musicPlayer.playerCodecAlgorithm?.play(path)
}

play(fileAtPath: "Djentuggah.mp3")
play(fileAtPath: "Procrastinallica.ogg")

В приведенном выше примере также показан простейший пример фабрики (переключение типа кодека от расширения файла)
Важно отметить что паттерн Стратегия не создает объекты, только лишь описывает способ создания общего интерфейса для переключения семейства алгоритмов.

Источники

https://refactoring.guru/ru/design-patterns/strategy

Исходный код

https://gitlab.com/demensdeum/patterns/

Паттерн Итератор

В данной заметке я опишу паттерн Итератор.
Данный паттерн относится к поведенческим шаблонам проектирования.

Распечатай это

Допустим нам нужно распечатать список треков с альбома “Procrastinate them all” группы “Procrastinallica”.
Наивная имплементация (Swift) выглядит так:

for i=0; i < tracks.count; i++ {
    print(tracks[i].title)
}

Вдруг при сборке обнаруживается что класс объекта tracks не отдает количество треков в вызове count, мало того, еще и к его элементам нельзя обратиться по индексу. Ой…

Отфильтруй

Допустим мы пишем статью для журнала “Wacky Hammer”, нам нужен список треков группы “Djentuggah” в которых bpm превышает 140 ударов в минуту. Интересная особенность этой группы, что ее записи хранятся в огромной коллекции underground групп, не отсортированная по альбомам, или по каким-либо другим признакам.
Представим себе что работаем с языком без функциональных возможностей:

var djentuggahFastTracks = [Track]()

for track in undergroundCollectionTracks {
    if track.band.title == "Djentuggah" && track.info.bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

Вдруг в коллекции оцифрованных кассет обнаруживается пару треков группы, и редактор журнала предлагает найти в этой коллекции треки и написать о них.
Знакомый Data Scientist сообщает, что вооружившись ML алгоритмом классификации треков Djentuggah можно будет не прослушивать коллекцию из 200 тысяч кассет вручную, а определить их автоматически.
Попробуем:

var djentuggahFastTracks = [Track]()

for track in undergroundCollectionTracks {
    if track.band.title == "Djentuggah" && track.info.bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

let tracksClassifier = TracksClassifier()
let bpmClassifier = BPMClassifier()

for track in cassetsTracks {
    if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

Ошибаемся

Теперь перед самой отправкой в печать, редактор сообщает что 140 ударов в минуту вышли из моды, людей больше интересуют 160, поэтому статью надо переписать, добавив необходимые треки.
Переписываем:

var djentuggahFastTracks = [Track]()

for track in undergroundCollectionTracks {
    if track.band.title == "Djentuggah" && track.info.bpm == 160 {
        djentuggahFastTracks.append(track)
    }
}

let tracksClassifier = TracksClassifier()
let bpmClassifier = BPMClassifier()

for track in cassetsTracks {
    if tracksClassifier.classify(track).band.title == "Djentuggah" && bpmClassifier.classify(track).bpm == 140 {
        djentuggahFastTracks.append(track)
    }
}

Самые внимательные заметили ошибку, параметр bpm изменен только для первого прохода по списку. Если бы проходов по коллекциям было больше, то и шанс ошибится был бы выше, именно потому нужно использовать принцип DRY.
Приведенный выше пример можно развивать и дальше, например добавив условие что нужно найти несколько групп с разным bpm, по именам вокалистов, гитаристов, это будет увеличивать шанс ошибки из-за дублирования кода.

Добавляем итератор

В литературе итератор описывается как совокупность двух протоколов/интерфейсов, первый это интерфейс итератора состоящий из двух методов – next(), hasNext()
next() отдает обьект из коллекции, а hasNext() сообщает что дальше есть объект и список не закончился.
Однако на практике я наблюдал итераторы с одним методом – next(), когда список заканчивался, из этого обьекта возвращался null.
Второй это коллекция которая должна иметь интерфейс отдающий итератор – метод iterator(), есть вариации с интерфейсом коллекции которая возвращает итератор в начальной позиции и в конечной – методы begin() и end() – используется в C++ std.
Использование итератора в приведенном выше примере позволит убрать дублирование кода, устранит шанс ошибиться из-за дублирования условий фильтрации. Также будет проще работать с коллекцией треков по единому интерфейсу – при изменении внутренней структуры коллекции, интерфейс останется старым и внешний код затронут не будет.

Перепишем:

let bandFilter = Filter(key: "band", value: "Djentuggah")
let bpmFilter = Filter(key: "bpm", value: 140)
let iterator = tracksCollection.filterableIterator(filters: [bandFilter, bpmFilter])

while let track = iterator.next() {
    print("\(track.band) - \(track.title)")
}

Изменение коллекции и я

Во время работы итератора коллекция может измениться, таким образом приводя внутренний счетчик итератора в некорректное состояние и вообще ломая такое понятие как “следующий объект”. Многие фреймворки содержат проверку на изменение состояние коллекции, и в случае изменений возвращают ошибку/exception. Некоторые реализации позволяют удалять объекты из коллекции во время работы итератора, предоставляя метод remove() в итераторе.

Источники

https://refactoring.guru/ru/design-patterns/iterator

Исходный код

https://gitlab.com/demensdeum/patterns/

Паттерн “Снимок”

В данной заметке я опишу паттерн “Снимок” или “Memento”

Данный паттерн относится к “Поведенческим” шаблонам проектирования.

Допустим мы разрабатываем графический редактор, и нам нужно добавить возможность откатывать действия по команде пользователя. Также очень важно чтобы у компонентов системы не было доступа к внутреннему состоянию откатываемых “действий”, при реализации данного паттерна у других компонентов системы есть доступ только к обьекту-снимку без возможности менять его внутреннее состояние, с предоставлением понятного, простого внешнего интерфейса . Для решения данной задачи используется паттерн “Снимок” или “Хранитель”.

Пример работы “Снимка” представлен ниже:


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

  1. Канва (canvas) на котором отображаются спрайты, графический интерфейс.
  2. Контроллер экрана, он обрабатывает нажатия и управляет логикой экрана.
  3. Состояния канвы которые сохраняются при каждом изменении, откатываются при необходимости с помощью контроллера экрана.

В разрезе паттерна “Снимок” классы представляют из себя:

  1. Канва – источник, состояния этого класса сохраняются как “снимки”, для последующего отката по запросу. Также источник обязан уметь восстанавливать состояние при передаче ему “снимка”.
  2. Контроллер – хранитель, этот класс знает как и когда сохранять/откатывать состояния.
  3. Состояние – снимок, класс который хранит состояние источника, плюс информацию о дате или индекс, по которому можно точно установить порядок для отката.

Важная особенность паттерна состоит в том, что иметь доступ к внутренним полям сохраненного состояния в снимке должен только источник, это необходимо для защиты снимков от изменений из вне (от рукастых разработчиков, желающих что-то поменять в обход инкапсуляции, ломающих логику системы). Для реализации инкапсуляции используют встраиваемые классы, а в C++ применяют возможность задания friend классов. Лично я реализовал простую версию без инкапсуляции для Rise, и с использованием Generic при реализации для Swift. В моем варианте – Memento отдает свой внутренний стейт только сущностям одного со стейтом класса:

Источники

https://refactoring.guru/design-patterns/memento

Исходный код

https://gitlab.com/demensdeum/patterns/

Visitor паттерн

В данной заметке я опишу паттерн проектирования под названием “Посетитель” или “Visitor”
Данный паттерн относится к группе Поведенических шаблонов.

Придумаем проблему

В основном, данный паттерн используют для обхода ограничения одиночной диспетчеризации (“single dispatch”), в языках с ранним связыванием.

Alice X by NFGPhoto (CC-2.0)
Создадим абстрактный класс/протокол Band, сделаем подкласс MurpleDeep, создадим класс Visitor с двумя методами – один для вывода в консоль любого наследника Band, второй для вывода любого MurpleDeep, главное чтобы имена (сигнатуры) у методов были одинаковые, а аргументы различались только классом. Через промежуточный метод printout с аргументом Band, создадим экземпляр Visitor и вызовем метод visit для MurpleDeep.
Далее код на Kotlin:

В выводе будет “This is Band class

Да как так то?!

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

Решаем проблему

Для решения данной проблемы существует множество решений, далее рассмотрим решение с помощью паттерна Visitor.
В абстрактный класс/протокол добавляем метод accept с аргументом Visitor, внутри метода вызываем visitor.visit(this), после этого добавляем в класс MurpleDeep оверайд/имплементацию метода accept, решительно и спокойно нарушая DRY, снова пишем visitor.visit(this).
Итоговый код:

Источники

https://refactoring.guru/ru/design-patterns/visitor-double-dispatch

Исходный код

https://gitlab.com/demensdeum/patterns

Flyweight паттерн

В данной заметке я опишу структурный паттерн “Легковес” или “Приспособленец” (Flyweight)
Данный паттерн относится к группе Структурных шаблонов.

Рассмотрим пример работы паттерна ниже:


Зачем он нужен? Для экономии оперативной памяти. Соглашусь что во времена повсеместного использования Java (которое потребляет cpu и память просто так), это уже и не так уж важно, однако использовать стоит.
На приведенном выше примере выводится только 40 объектов, но если поднять их количество до 120000, то потребление памяти увеличится соответствующе.
Посмотрим на потребление памяти без использования паттерна flyweight в браузере Chromium:

Без использования паттерна потребление памяти составляет ~300 мегабайт.

Теперь добавим в приложение паттерн и посмотрим потребление памяти:

С использованием паттерна потребление памяти составляет ~200 мегабайт, таким образом мы сэкономили 100 мегабайт памяти в тестовом приложении, в серьезных проектах разница может быть гораздо больше.

Как работает?

В приведенном выше примере мы отрисовываем 40 котиков или для наглядности 120 тысяч. Каждый котик загружается в память в виде png изображения, далее в большинстве рендеров оно конвертируется в битовую карту для отрисовки (фактически bmp), делается это для скорости, так как сжатый png очень долго отрисовывается. Без использования паттерна мы загружаем 120 тысяч картинок котиков в оперативную память и рисуем, а вот при использовании паттерна “легковес” мы загружаем в память одного котика и рисуем его 120 тысяч раз с разной позицией и прозрачностью. Вся магия состоит в том, что координаты и прозрачность мы реализуем отдельно от изображения кота, при отрисовке рендер берет всего одного котика и использует объект с координатами и прозрачностью для корректной отрисовки.

Как выглядит в коде?

Ниже приведены примеры для языка Rise

Без использования паттерна:


Картинка кота загружается для каждого объекта в цикле отдельно – catImage.

С использованием паттерна:

Одна картинка кота используется 120 тысячами объектов.

Где используется?

Используется в GUI фреймворках, например у Apple в системе “переиспользования” (reuse) ячеек таблиц UITableViewCell, чем поднимают порог вхождения для новичков которые не знают про этот паттерн. Также повсеместно используется в разработке игр.

Исходный код

https://gitlab.com/demensdeum/patterns/

Источники

https://refactoring.guru/ru/design-patterns/flyweight
http://gameprogrammingpatterns.com/flyweight.html

Хороший, плохой, мерзкий синглтон

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


Wolf in sheep’s clothing by SarahRichterArt

История происходит в альтернативной вселенной, все совпадения случайны…

Погладь кота на дому с Cat@Home

У каждого человека иногда в жизни возникает непреодолимое желание погладить кота. Аналитики всего мира пророчат что первый стартап создавший приложение по доставке и аренде котиков станет крайне популярным, в недалекой перспективе будет куплен компанией Moogle за триллионы долларов. Вскоре так и происходит – парень из Тюмени создает приложение Cat@Home, и вскоре становится триллиардером, компания Moogle получает себе новый источник прибыли, а миллионы застрессованых людей получают возможность заказать кота на дом для дальнейшего глаженья и успокоения.

Атака клонов

Крайне богатый дантист из Мурманска Алексей Голобородько, впечатлившись статьей про Cat@Home из Фorbes, решает что тоже хочет быть астрономически богатым. Для достижения этой цели, через своих друзей, он находит компанию из Голдфилда – Wakeboard DevPops которая оказывает услуги по разработке ПО, он заказывает разработку клона Cat@Home у них.

Команда победителей

Проект называют Fur&Pure, поручают талантливой команде разработчиков из 20 человек; далее сосредоточимся на группе мобильной разработки из 5 человек. Каждый член команды получает свою часть работы, вооружившись agile-ом и скрамом, команда завершает разработку в срок (за полгода), без багов, релизит приложение в iStore, где ее оценивают 100.000 пользователей на 5, много комментариев о том как прекрасно приложение, как прекрасен сервис (Альтернативная вселенная как-никак). Коты выглажены, приложение выпущено, вроде-бы все идет хорошо. Однако компания Moogle не торопится покупать стартап за триллионы долларов, потому что в Cat@Home уже появились не только коты но и собаки.

Собака лает, караван идет

Владелец приложения решает что пора добавить в приложение собак, обращается за оценкой в компанию и получает примерно минимум полгода на добавление собак в приложение. Фактически приложение будет написано с нуля снова. За это время Moogle добавит в приложение змей, пауков и морских свинок, а Fur&Pur получит только собак.
Почему так получилось? Во всем виновато отсутствие гибкой архитектуры приложения, одним из самых распространенных факторов является антипаттерн проектирования Singleton.

А что такого?

Для того чтобы заказать кота на дом, потребителю нужно создать заявку и отправить ее в офис, где в офисе ее обработают и пришлют курьера с котом, курьер уже получит оплату за услугу.
Один из программистов решает создать класс “ЗаявкаНаКота” с необходимыми полями, выносит этот класс в глобальное пространство приложения через синглтон. Зачем он это делает? Для экономии времени (копеечная экономия получаса), ведь проще вынести заявку в общий доступ, чем продумывать архитектуру приложения и использовать dependency injection. Дальше остальные разработчики подхватывают этот глобальный объект и привязывают свои классы к нему. Например все экраны сами обращаются к глобальному объекту “ЗаявкаНаКота” и показывают данные по заявке. В итоге такое монолитное приложение тестируется и сдается в релиз.
Все вроде хорошо, но вдруг появляется заказчик с требованием добавить в приложение заявки на собак. Команда судорожно начинает оценивать сколько компонентов в системе затронет данное изменение. По окончанию анализа оказывается что нужно переделать от 60 до 90% кода, чтобы научить приложение принимать в глобальном объекте-синглтоне не только “ЗаявкуНаКота” но и “ЗаявкуНаСобаку”, оценивать добавление остальных животных на данном этапе уже бесполезно, справиться хотя бы с двумя.

Как не допустить синглтон

Во-первых, на этапе сбора требований явно указать необходимость в создании гибкой, расширяемой архитектуры. Во-вторых, стоит проводить независимую экспертизу кода продукта на стороне, с обязательным исследованием слабых мест. Если вы разработчик и вы любите синглтоны, то предлагаю одуматься пока не поздно, иначе бессонные ночи и выжженные нервы обеспечены. Если вы работаете с проектом по наследству, в котором много синглтонов, то попытайтесь избавиться от них как можно быстрее, или от проекта.
Переходить с антипаттерна синглтонов-глобальных объектов/переменных нужно на dependency injection – простейший паттерн проектирования в котором все необходимые данные задаются экземпляру класса на этапе инициализации, без дальнейшей необходимости быть привязанным к глобальному пространству.

Источники

https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons
http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/
https://blog.ndepend.com/singleton-pattern-costs/