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

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

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

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

Пример на 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

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

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

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

Death-Mask 1.0!

Сегодня выходит релиз версия игры Death-Mask, созданная с нуля вместе с библиотеками Flame Steel Engine, Flame Steel Engine Game Toolkit и другими.

https://demensdeum.com/games/deathMask/

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

Обновления 3д моделей и небольшие геймплейные изменения возможны в ближайшем будущем.

Маска Смерти – киберфентезийное приключение в бесконечном технолабиринте. Приготовься умереть!

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

Паттерн Наблюдатель (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

Death-Mask Wild Beta

Игра Death-Mask переходит в статус публичной беты (wild beta)
Переработан экран главного меню игры, добавлен вид на синюю зону технолабиринта, с приятной музыкой на фоне.

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