Паттерны 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#

class Application {
        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#

interface IExpression
{
    String interpret(Context context);
}

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

class ConstantExpression : TerminalExpression
{
    private String constant;

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

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

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

class PerformExpression : NonTerminalExpression
{
    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:

run input = 
  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: ParseLeafsState -> ParseLeafsState
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 monster = new Monster();
auto weapon = new Weapon(“Claws”);
monster->weapon = weapon;
auto health = new MonsterHealth(100);
monster->health = health;

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

auto monster = new MonsterBuilder()
                  .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