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

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

Сунь Цзы

Допустим мы разрабатываем музыкальный плеер со встроенными кодеками. Под встроенными кодеками подразумеваяется чтение музыкальных форматов без использования внешних источников операционной системы (кодеков), плеер должен уметь читать треки разных форматов и воспроизводить их. Такими возможностями обладает плеер 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/

Реклама

Loading...

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

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

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

Допустим нам нужно распечатать список треков с альбома “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/

Death Mask дев отчет 1

Новая непостоянная рубрика “дневники разработчиков” или Dev Diary на иностранный манер.
Разработка игры Death-Mask идет полным ходом, за 2019 год добавлен логотип движка игры Flame Steel Engine, экран выбора начальной карты по островам (зеленый, красный, черный, белый) вывод текстур для стен, потолка, пола лабиринта, увеличены размеры игровой зоны.


Карта Города Красной Зоны

Далее планируется добавить 3д модели для окружения, вместо Doom-style спрайтов, в планах добавить модели для оружия, ящиков, врагов, друзей. В геймплее планируется добавить валюту, магазины, возможность покупать части игровой карты с указанием интересных мест с лутом, и возможного нахождения “Маски Смерти”. Также хочу добавить возможность нанимать компаньонов для странствий по киберлабиринту.
Следите за новостями.