Русский
Русский
English
Статистика
Реклама

Архитектура по

Ежедневный стэндап для архитектора с оркестром

09.05.2021 00:17:30 | Автор: admin

Будни разработчика. Цели определены, направления выбраны, задачи разжеваны. Нужно просто писать код и жевать кашку. Что может скрасить серость и однообразность существования? Конечно же daily standup - шоу, в котором есть место для каждого! Ну вот эти вот неожиданные я посмотрел архитектуру и там ошибка или вот я добавил новый модуль, который нам может пригодиться в будущем ну и, конечно, я сделал всё проще и быстрей. Мы ведь именно ради этого делаем все церемонии груминга и планирования. Чтоб как бы подготовить почву и дать всем время посидеть молча и подготовить эти панчлайны на конец спринта. А самое обидное, что, потратив столько усилий, на сам стендап вы обычно не попадаете и панчи вам передаёт ваше начальство.

http://personeltest.ru/aways/www.flickr.com/photos/cosmic_flurk/5712236914@CCLhttps://www.flickr.com/photos/cosmic_flurk/5712236914@CCL

В чём интрига? В самой сцене. В самом начале дня у нас обычно дейли разработчиков. Там все делятся успехами и проблемами. Охотно так делятся. Долго и много. Как раковые клетки. Даже анализы можно делать. Чем больше тем на этом daily, тем больше опухоль в проекте. Потом придётся выжигать и резать. Но команд много, а архитектора мало. Поэтому его не зовут, если не уже дымится.

Дальше отсеиваются разработчики и в следующий тур идут только тимлиды. Они более практичны и рассказывают о своих успехах и как всё по плану, а проблемы выносят в оффлайн. Сухо так и без шуток. С dev. менеджером обычно всё позитивно, если менеджер сам не станет копать или не возникнет подозрение о том, что дедлайн слишком близко к носу. И так как начальник - человек занятой (его каждый раз кто-то занимает, но, как ни странно, каждый раз и отдаёт), команд много и проекты глобальные, то представитель от каждого сайта (ротация среди лидов) подключается к общему митингу, куда обычно и приглашают архитектора. Это самые бесполезные пол часа. Каждый уже пару раз рассказал свою историю и пережевал проблемы (а тут, видимо, как и с шутками: проблема, повторенная дважды, перестаёт быть проблемой). Вдобавок говорить то на английском надо, который для большинства неродной и куча акцентов Короче, каждый chosen one пытается сказать одно-два предложения, так чтоб не дай бог не задали вопросов. Как на приёме в налоговой.

Сетка игр в дейлиСетка игр в дейли

В итоге каждая команда поделилась своими проблемами только с собой. Оставшись внутри закрытого спектра бизнес-задач и решений, которые знакомы всем её участникам. Я не против психотерапии, но если у вас проблема и вы можете решить её внутри команды, то зачем ждать планёрки? А если не можете то, чем поможет её озвучить в том же кругу? Но я не менеджер и даже не Agile тренер мне не понять. Я - Винни Пух и по утрам хожу в гости. К кому-нибудь. Желательно без приглашения (иначе рискуете услышать заученный текст на английском) на ту самую первую сцену, где профилактика будет стоить дешевле лечения. Это здорово экономит время, так как альтернативой будет просмотр коммитов. Код ревью тот же рыбий жир. Может и полезно, но никто не любит. Любят мёд. Но мёд, как известно, странный предмет он есть лишь там, где тебя нет.

Поправка на ветер

Не все компании одинаковы. Как и фломастеры. Кое где есть много архитекторов и мало команд. Кое где нет трёх уровней сцены. Возможно, нет никаких трудностей с коммуникацией и прозрачностью implementation details, в которых дьявол. Так что пробуйте на вкус и цвет. Если нет проблем, то и жизнь хороша. Но как говорил агент Смит, в утопии можно потерять весь урожай. С местной командой всегда есть хороший контакт и неформальные связи. Разработчик может просто подойти и спросить. А вот если у вас куча удаленных единиц и они в какой-нибудь далёкой стране в другом часовом поясе, то с ними очень тяжело наладить прямой контакт. Особенно там, где очень уважают корпоративную иерархию и не умеют говорить нет. Не положено у них программистам подавать голос вне церемоний и без участия прямого начальника.

Из личного опыта: иногда доходит до абсурда. Был проект, где архитектором была девушка. А on-site менеджером в далёком стане усатый седой дядька с чалмой. Так вот пока кто-то из команды архитекторов-мужчин не посылал мейл: сделай как она сказала - раджа игнорировал все её заметки. Культур такой. В общем мудрый раджа оказался тем ещё мудрилой.

В чём же будни архитектора, помимо кофе и рисования? Ведь многие считают, что гнездо архитекторы вьют только из одноразовых стаканчиков в местах большого скопления whiteboard-ов. Конечно же в ловле личинок bug-ов! В первую очередь, конечно, интересуют ошибки проектирования. То есть свои же собственные промахи. До начала разработки архитектуру семь раз замеряли, но когда приходит время отрезать, выясняется, что попасть в метку тяжело, пила не та, то брус не тот. Все стройные теории начинают сползать с вакуумного глобуса. На этапе разработки первичной версии начинают выясняться все детали, которые попали в финальную версию контракта. Самые главные: дата и точный спектр функционала первого демо для клиента.

Первые конфликты требований не заставят себя ждать. Проблемы сами по себе очень пунктуальны никогда не запаздывают и даже стараются встретиться с вами пораньше. Мои наблюдения за природой событий подсказывают, что источником часто являются благие намерения и ограниченные знания (слабоумие и доброта). Чем меньше знаешь деталей, тем радужней и проще картинка. Поэтому клиенту обещают чуть больше и чуть раньше. Даже если не официально. Чтоб зарекомендовать себя, выиграть лишний бал для торгов на следующей фазе или обезопасить дедлайн/качество, насильно передвинув срок сдачи на пару недель раньше. Ну чтоб оставить чуток на допилинивание и стабилизацию. Спринт на закалку. Английское харденинг более уместно. Как только начинают двигать дедлайны взад-вперед, сразу же чувствуется харденинг. И если дёргать дальше, то проект может плохо кончить. Те же хорошие мысли подталкивают участников процесса добавить немного еще функционала из серии чтоб два раза не ходить. Такие вещи из бэклога, которые в том же куске что разрабатывается сейчас и весят мало. И как бы проверками их покроют всё равно и код уже открыт на нужном месте. Мелкие задания. Пару часов тут, пару часов там и вот уже появляются первые участки дороги в ад инженера.

7 фишек этому господину!7 фишек этому господину!

Ад у каждого свой. Мой похож на ежедневные диспуты с начальниками разных отделов, каждый из которых предлагает вместо твоей глупой инфраструктуры, давай сделаем анимированные кнопочки. Случалось же слышать: а юнит тесты точно нужно писать? А может можно как-то без них? или нам же сейчас не нужно это твоё скалкобилити, а красивый интерфейс это лицо продукта. Хорошее лицо тоже важно, но этот орган плохо поглощает кинетическую энергию и при встрече с бетонной реальностью лучше приземлиться на пятую точку. Особенную нишу занимают вот тут 20 дней на безопасность выделено, нужно сделать за 10, чтоб ещё пару фишек втиснуить из бессмертной серии а семь шапок можешь?. Мне всегда сложно объяснить кому-то почему же 2+2 = 4. На сложные вещи аргументация всегда находится, а как объяснить то, что ты считаешь базовым, да еще и когда экспериментально доказать за 5 минут не получится.

Флешбэнг

Вспоминается лектор в универе по линейной алгебре, который заставлял писать на экзамене типа один из результатов 2 + 2 будет 4. Точное определение операции сложения и поля чисел, на котором эта операция произведена указаны ниже. В противном случае сам давал произвольное определение так, чтоб 2+2 было 4+С или допустим (+4;-4) и не засчитывал ответ. Я тогда бесился, но похоже препод был настоящим инженером. Не даром его книги продают в переводах на несколько языков. Даром пишу тут я и пытаюсь передать его мысль: решений зачастую больше, чем одно и в разных условиях правильными могут считаться разные варианты.

Чтобы понизить градус ада приходиться заниматься презентациями, которые бы наглядно показали как у нас всё работает и почему. Хочется, конечно, Босха, но приходится сводить простые зарисовки со стрелочками. Анализировать бэклог, искать пересекающиеся требования и показывать, что кнопочки сейчас по затратам равны кнопочки завтра, а безопасность сейчас намного дешевле безопасность завтра. Строится цепочка требований ведущих или исходящих из кнопочки и такая же для требования безопасности. Так как бизнес-требования обычно не пересекаются, то цепочку таких требований легко двигать в плане вперёд и назад. А значит и цена будет неизменной. Инфраструктура же пронизывает все модули и цепочки, а значит если завтра будет в 10 раз больше модулей, то будет и в 10 раз больше интеграции/внедрения и проверок (важная статья расходов и времени). Кто бы мог подумать!? И обязательно гуглите что-то из серии цена ошибки безопасности в своём промышленном/деловом секторе. Для производства всегда хорошо работает пример Иранских центрифуг. В финтехе или ритейле примеров полным-полно. Хорошая страшилка лучше любых формул. Детей пугают сказкой про волка, а не расчётами семейного бюджета (хотя последнее может быть намного страшней).

инфраструктура шашлыкаинфраструктура шашлыкаНа моей памяти

Лет 10 назад из наших клиентов лидер в своём секторе в развитой европейской стране, спустился на пару пунктов в рейтинге и обороте после того, как его взломали. А другой наш клиент, бывший на третьем месте незамедлительно вложил тройной бюджет в безопасность и в том же году возглавил рейтинг. Хотя по размеру и размаху он раза был в 4 меньше. Сейчас проверил, они спустились на второе место, но с хорошим отрывом от их прежнего третьего.

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

Мой know-how в этом процессе направляемый мозговой штурм. У вас есть готовый вариант архитектуры модуля. В моём случае это значит, что определена модель (абстракции) и контракты (данные) и есть сопутствующая диаграмма взаимодействий или сущностей. Так вот эту диаграмму я начинаю строить с разработчиком, скармливая ему требования и проблемные аспекты так, чтоб он посмотрел в конце на моё решение и сказал классно мы с тобой придумали!. Ну ещё бы.

Работает это примерно так:

Нужен модуль, который получает пакет цифровых документов и мапит их в базу. Разработчик сразу предлагает какой-нибудь pub-sub, асинхронные (так почему-то 80% людей называют параллельные процессы) таски и пару микросервисов. Куда же без них.

первый предложенный вариантпервый предложенный вариант

Теперь надо подхватить его под руку и вести туда, куда хотите вы. Закидывается проблема и предлагается решение, которое у вас прописано в архитектуре. Как-то так: Хорошее решение, но если у нас будет асинхронный маппинг, то возникнут проблемы совместного доступа. А что, если вместо параллельности мы сделаем последовательность?.

второй предложенный вариантвторой предложенный вариантПро параллельность

Иногда пропускают мимо глаз, ушей и, в конечном счёте, рук проблемы последовательных запросов, перерастающих из очереди в параллелизм. То есть с точки зрения бизнеса там всё в строгом порядке и один за одним. Классический (уже для меня) пример обработка конвейера. Вот есть производственный конвейер, на ленте которого изделия идут на следующий этап, а рядом есть камера, которая снимает каждую деталь для обработки данных в какой-нибудь отдельной системе. Система эта и камера отдельный продукт для мониторинга и в производстве не участвует, так что лента никого не ждёт. Потом наступает тот самый неочевидный момент, когда скорость ленты выше скорости обработки изображения. Бегущие напрямую в бэкенд запросы идут внахлест. Закон Литтла в действии. И если никто об этом не подумал, то у нас будут всё прелести конкуренции за ресурс, превратившийся из эксклюзивного в общий.

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

конечнаяконечная

За шага 3-4 мы уже достигаем желаемого результата. В отличии от варианта, когда вы просто объясняете почему стоит сделать именно так, вы не лишаете человека самого важного поиска решения. Разработчик не стенографист и не стал экспертом потому, что просто быстро записывал за кем-то построчно. Если лишить его загадки, то работа может вызвать протест и кучу отрицательных эмоций. С другой стороны, он не знает всей картины. Ни того, что и как будут делать в других командах, ни того, что запланировано на полгода вперёд. В этом основная причина существования должности архитектора широкий и далекий взгляд на проект. Но ничего не получится, если ваша архитектура не обоснована требованиями. Нечем будет вести разработчика, да и незачем. В таком случае лучше честно партбилет на стол. У нас с этим строго. Это по утрам я Винни-Пух, а к концу дня я свинья с ружжом!

архитектор бьёт чучелом ружья вымышленного леопардаархитектор бьёт чучелом ружья вымышленного леопарда

И стрелять всегда есть в кого. Пока все усердно пишут код нужно следить, что б всё неусерднулось. Тут на помощь нам всякие индикаторы и статические анализаторы кода. Делать метрики для программистов удел менеджеров. Я лично озабочен метриками кода. Индикаторы здоровья проекта:

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

  • Длинные функции и классы не плохой индикатор поспешной работы и костылей. На начальном этапе такого быть не должно.

  • Сложность кода можно отследить по количеству юнит тестов при высоком покрытые. Много тестов значит много веток, а значит опять что-то попахивает.

  • Нарушение конвенций тоже стоит заавтоматить и следить. Закладывается фундамент дома, в котором нам всем жить долго.

  • Много статики (классов, методов, полей), обычно, проявления антипаттернов.

  • И, конечно, проверки безопасности на вшитые пароли и вшивые референсы на какие-нибудь опенсорсы с уязвимостями или сложными лицензиями.

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

Для подготовки к следующему этапу необходимо выстроить хороший механизм логирования. Желательно на два уровня один с понятной историей для бизнеса, а второй для технического анализа, что же всё-таки навернулось. Когда со стадии демо мы шагнём, пусть и в ограниченный, но production, то там будут первые кострища и охота на ведьм. Наблюдать в полнолуние как кто-то седеющими руками набирает код прям в проде плохая примета. По возможности старайтесь избегать этого. Так что одним стримом пишем: пользователь ****341 запросил функцию Поиск Заказа с аргументом 3480-4341-1334. Результат: не найдено. На этот лог сядет саппорт, когда процесс встанет. А второй стрим со стеком, сервисами и всеми деталями (опять-таки, кроме тех, что нужно замазать ради безопасности или для сохранения личных и коммерческих секретов). Тут уже пойдут разработчики, когда процесс ляжет. Логи делать необходимо с самого начала. Они всегда нужны вчера, когда всё случилось, а вот завтра от них будет мало толку. Клиент как-то не любит слышать: У вас тут вчера производство встало, а мы не знаем почему. Но когда встанет в следующий раз у нас будет больше данных для анализа!. Хотя в некоторых компаниях лишь под угрозой штрафов начинают понимать, что стоило вложиться сразу в аудит и мониторинг да начинают чесать головы (те в которые едят и те на которых сидят).

Мысль вслух

Есть разные архитекторы. Сейчас самый хайповый архитектор решений. А нужен то архитектор проблем. Тот, кто предскажет нестыковки и видит конфликты. Опытный разработчик справиться с решением любой правильно поставленной задачей и сам. Архитектор необходим чтобы сделать задачу правильной и полной (не бодипозитивной, а с жирными мазками в местах болевых точек и сочным описанием того, что случиться, если на точку нажмут). Мне не важно пустой ли на половину стакан или полный. Мне важно определить, что значит половина и, что стакан будет в том месте и времени, когда начнут лить. И стоит уточнить, что лить будут не серную кислоту в бумажный стаканчик.

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


Подробнее..

Пишем под android с Elmslie

20.04.2021 08:05:05 | Автор: admin

Вступление

Это третья часть серии статей об архитектуре android приложения vivid.money. В ней мы расскажем об Elmslie - библиотеке для написания кода под android с использованияем ELM архитектуры. Мы назвали ее в честь Джорджа Эльмсли, шотландского архитектора. С сегодняшнего дня она доступна в open source. Это реализация TEA/ELM архитектуры на kotlin поддержкой android. В первой статье мы рассказали о том почему выбрали ELM. Перед прочтением этой статьи лучше ознакомиться как минимум со второй частью, в которой мы более подробно рассказывали том собственно такое ELM.

Оглавление

Что будем писать

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

Модель

Написание экрана проще начинать с проектирования моделей. Для каждого экрана нужны State, Effect, Command и Event. Рассмотрим каждый из них по очереди:

State

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

На нашем экране будет отображаться либо числовое значение, либо состояние загрузки. Это можно задать двумя полями в классе: val isLoading: Boolean и val value: Int?. Для удобства изменения, State лучше реализовывать как data class. В итоге получается так:

data class State(  val isLoading: Boolean = false,  val value: Int? = null)

Effect

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

В нашем примере единственной командой UI будет показ Snackbar при ошибке загрузки value. Для этого заведем Effect ShowError. Для удобства Effect можно создавать как sealed class, чтобы не забыть обработать новые добавленные эффекты:

sealed class Effect {  object ShowError : Effect()}

Command

Каждая Command обозначает одну асинхронную операцию. Подробнее о том как они обрабатываются расскажем чуть позже. В результате выполнения команды получаются события, которые в свою очередь повлияют на состояние экрана.

У нас будет одна операция - загрузить данные. Эту Command назовем LoadValue. Команды так же удобнее задавать как sealed class:

sealed class Command {  object LoadValue : Command()} 

Event

Все события, которые влияют на состояние и действия на экране: Ui: ЖЦ экрана, взаимодействие с пользователем, все что приходит из View слоя Internal: Результаты операций с бизнес логикой

Теперь перейдем к событиям. В нашем проекте мы разделяем события на две категории:

  • Event.UI: все события, которые происходят во View слое

  • Event.Internal: результаты выполнения команд в Actor.

В этом примере будет два UI события: Init - открытие экрана и ReloadClick - нажатие на кнопку обновления значение. Internal события тоже два: ValueLoadingSuccess - успешный результат Command LoadValue и ValueLoadingError, которое будет отправляться при ошибке загрузки значения.

Если использовать разделение на UI и Internal, то Event удобнее задавать как иерархию sealed class:

sealed class Event {  sealed class Ui : Event() {    object Init : Ui()    object ReloadClick : Ui()  }     sealed class Internal : Event() {    data class ValueLoadingSuccess(val value: Int) : Internal()    object ValueLoadingError : Internal()  }}

Реализуем Store

Закончив с моделями, перейдем собственно к написанию кода. Сам Store реализовывать не нужно, он предоставляется библиотекой классом ElmStore.

Repository

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

object ValueRepository {private val random = Random()fun getValue() = Single.timer(2, TimeUnit.SECONDS)    .map { random.nextInt() }    .doOnSuccess { if (it % 3 == 0) error("Simulate unexpected error") }}

Actor

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

Для его создания нужно реализовать интерфейс Actor, который предоставляется библиотекой. Actor получает на вход Command, а результатом его работы должен быть Observable<Event>, с событиями, которые сразу будут отправлены в Reducer. Для удобства в библиотеке есть функции mapEvents, mapSuccessEvent, mapErrorEvent и ignoreEvents, которые позволяют преобразовать данные в Event.

В нашем случае Actor будет выполнять только одну команду. При выполнении команды загрузки мы будем обращаться к репозиторию. В случае получения успешного значения будет оправляться событие ValueLoaded, а при ошибке ErrorLoadingValue. B итоге получается такая реализация:

class Actor : Actor<Command, Event> {override fun execute(command: Command): Observable&lt;Event&gt; = when (command) {    is Command.LoadNewValue -&gt; ValueRepository.getValue()        .mapEvents(Internal::ValueLoaded, Internal.ErrorLoadingValue)}}

Reducer

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

В этом классе нужно реализовать функцию reduce для обработки событий. Помимо вашей логики в Reducer можно использовать 3 функции:

  • state - позволяет изменить состояние экрана

  • effects - отправляет эффект во View

  • commands - запускает команду в Actor

class Reducer : DslReducer<Event, State, Effect, Command>() {override fun Result.reducer(event: Event) = when (event) {    is Internal.ValueLoaded -&gt; {        state { copy(isLoading = false, value = event.value) }    }    is Internal.ErrorLoadingValue -&gt; {        state { copy(isLoading = false) }        effects { +Effect.ShowError }    }    is Ui.Init -&gt; {        state { copy(isLoading = true) }        commands { +Command.LoadNewValue }    }    is Ui.ClickReload -&gt; {        state { copy(isLoading = true, value = null) }        commands { +Command.LoadNewValue }    }}}

Собираем Store

После того как написаны все компоненты нужно создать сам Store:

fun storeFactory() = ElmStore(    initialState = State(),    reducer = MyReducer(),    actor = MyActor()).start()

Экран

Для написания android приложений в elmslie есть отдельный модуль elmslie-android, в котором предоставляются классы ElmFragment и ElmAсtivity. Они упрощают использование библиотеки и имеют схожий вид. В них нужно реализовать несколько методов:

  • val initEvent: Event - событие инициализации экрана

  • fun createStore(): Store - создает Store

  • fun render(state: State) - отрисовывает State на экране

  • fun handleEffect(effect: Effect) - обрабатывает side Effect

В нашем примере получается такая реализация:

class MainActivity : ElmActivity<Event, Effect, State>() {override val initEvent: Event = Event.Ui.Initoverride fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContentView(R.layout.activity_main)    findViewById&lt;Button&gt;(R.id.reload).setOnClickListener {        store.accept(Event.Ui.ClickReload)     }}override fun createStore() = storeFactory()override fun render(state: State) {    findViewById&lt;TextView&gt;(R.id.currentValue).text = when {        state.isLoading -&gt; &quot;Loading...&quot;        state.value == null -&gt; &quot;Value = Unknown&quot;        else -&gt; &quot;Value = ${state.value}&quot;    }}override fun handleEffect(effect: Effect) = when (effect) {    Effect.ShowError -&gt; Snackbar        .make(findViewById(R.id.content), &quot;Error!&quot;, Snackbar.LENGTH_SHORT)        .show()}}

Заключение

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

Подробнее..

Враг не пройдёт, или как помочь командам соблюдать стандарты разработки

24.09.2020 00:22:57 | Автор: admin
Подход governance as a code обеспечивает контроль соблюдения архитектурных принципов как в части конфигураций инфраструктуры, так и в части программного кода. Правила проверки каждого артефакта, будь то конфигурация k8s, список библиотек или даже описание сценария CI/CD, описаны специальным кодом проверки правил, имеют свой жизненный цикл, могут тестироваться и ничем не отличаются от обычного программного продукта.

Александр Токарев (Сбербанк) расскажет, как и что можно проверять в процессе разработки программного обеспечения, чтобы разрабатывать более безопасные и качественные приложения, и почему Сбербанк решил не использовать такие очевидные решения как SonarCube, а разработать собственное решение на базе Open Policy Agent без дополнительных пакетов над ним. Также Александр покажет, когда выбирать admission controller, когда использовать чистый Open Policy Agent, а когда можно обойтись без какого-либо контроля.

Александр поговорит о том, нужны ли стандарты, что такое язык Rego и что за крутой продукт Open Policy Agent, а также рассмотрит нетиповые кейсы его применения, как с ним работать, и как его использовать для контроля. Email Александра.



Итак, представьте Сбербанк:
  • Более 50 приложений в облачной платформе OpenShift в production;
  • Релизы не реже двух раз в месяц, а порой и чаще;
  • Ожидается не менее 20 новых приложений в год для перевода в Openshift;
  • И все это на фоне микросервисной архитектуры.

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

План перехода к автоматизации


Мы решили переложить все эти задачи на автоматизированный контроль и определили для себя следующие уровни зрелости автоматизированного контроля:
  1. Базовые проверки;
  2. Интеграция с контролем версий;
  3. Повторное использование проверок;
  4. Автоматическое применение проверок;
  5. Коррекции на основе проверок;
  6. Продвинутые проверки когда вы всему научились и можете писать очень крутые вещи.

Уровни построены так, что, если вы не достигли предыдущего, на следующий не перейдете.

Базовые проверки


Google предлагает начать их с Kubernetes и приводит нас к абсолютно старым и неживым проектам:



Можно обнаружить живые и развивающиеся проекты, но при их изучении понимаешь, что для результата надо написать очень много букв с точки зрения CRD, они жёстко прибиты к Kubernetes, у них нет DSL (потому что сложные проверки описываются через REGEXP), и при этом нет возможности дебага политик. Вот пример DSL такого продукта, который проверяет CPU и memory limit в кластере. Это абсолютно своя CRD, которая выводит обычное сообщение, и далее нужно много букв и REGEXP для того, чтобы это контролировать:



Можно взять Sonar, но мы поняли, что для создания в нём элементарного правила нужно минимум 7 артефактов, а это довольно сложно:



После долгих поисков мы обнаружили замечательный продукт Open Policy Agent (OPA), который на самом деле движок политик, написанный на Go. Он очень быстрый, потому что inmemory. Им можно проверять всё что угодно с использованием собственного декларативного языка Rego, который не сложнее SQL. Можно использовать внешние данные и встраивать продукт куда угодно. Но самое главное мы можем управлять форматом ответа, когда это не просто boolean true/false, а всё, что мы захотим увидеть.



Простейшая проверка, проверяющая заполнение ревестов и лимитов, включает в себя получение всех контейнеров Kubernetes, которые объявлены в нашем YAML, их проверку на заполнение нужных полей (проходя по структуре YAML) и вывод сообщения (JSON). Если с проверкой всё хорошо, то в result не будет ничего. Три строчки кода и у вас она работает:



Конечно, объекты на продакшене гораздо более сложные, потому что надо что-то фильтровать, надо всё также получать списки контейнеров и работать с состоянием целого кластера. Дополнительно мы выводим диагностическое сообщение о том, пройдена или нет проверка, статус её прохождения, вид проверок (группа) и фактически тот объект, который тестировала проверка.

Примеры проверок K8S




Может создаться ощущение, что Open Policy Agent используется только для Kubernetes, но это не так. Проверять можно всё что угодно, лишь бы это был JSON:
  • Maven;
  • NPM;
  • Terraform;
  • Даже ER diagrams, потому что в том же Power Designer это XML.

Таким образом мы приходим к Open Policy Agent в Сбербанке:



Есть Open Policy Agent и есть система плагинов на Python (потому что на нем элементарно написать преобразования) и конечно же пользовательский интерфейс. Таким образом проверка абсолютно любой конфигурации (K8S YAML, Pom.xml, .properties, .ini) работает просто.

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



Вернемся к примеру maven:



Предположим, мы хотим контролировать использование spring-boot-starter-actuator для написания тех самых readinessProbe. Живая проверка очень проста: мы получаем все dependency, а дальше смотрим, что библиотека это spring-boot-starter-actuator. Как я уже говорил, большая часть проверок это вывод диагностических сообщений об их прохождении/непрохождении:



Однако бывают более сложные кейсы. Предположим, нам нужно проверить конфигурацию Kubernetes на наличие разрешённых image (fluentbit2, envoy3, nginx:1.7.9) и вывести имя запрещённого. Видим, что у нас есть валидная конфигурация с абстрактным nginx и есть конфигурация с запрещённым nginx:



Поищем в Google как это сделать. Само собой, первая ссылка приводит на сайт проекта, но, как принято в документации OPA, там нет информации о том, что нам надо:



Воспользуемся ссылкой от Amazon, они же профессионалы! Большой код, 19 строк букв где та самая лаконичность, которую я вам рекламировал раньше? Какие-то непонятные команды, палки что это? Долго-долго читаем документацию, понимаем, что это так называемый Object comprehension, и что на самом деле коллеги из Amazon транслируют массив как есть в список объектов. Почему? Потому, что из документации непонятно, что можно объявить объект таким элементарным и очевидным образом:



Тем не менее применяем и получаем результаты. Но всё равно очень много букв 13 строк кода!



А как же написать, чтобы было красиво, и чтобы пользоваться всей мощностью декларативности? На самом деле все очень просто: получаем контейнер Kubernetes, а дальше представляем, что у нас SQL и фактически данными строчками пишем not in. Мыслите в терминах SQL, будет гораздо проще. За 7 строчек кода мы получаем действительно нужную нам информацию все неразрешённые контейнеры name:



Где все это писать? Есть две тулзы:
Rego Playground
Она размещена в интернете авторами продукта и позволяет вести отладку онлайн. Она не всегда хорошо выполняет сложные проверки, но самое ценное её свойство для меня готовая библиотека проверок, где можно подсмотреть какие-то идеи:



Плагин Visual Studio Code
Действительно прекраснейший плагин. В нем есть всё, что надо для средств разработки:
  • Syntax check проверка синтаксиса;
  • Highlighting подсветка;
  • Evaluation вычисления проверок;
  • Trace трассировка;
  • Profile профилирование;
  • Unit tests запуск юнит-тестов



Интеграция


Теперь надо это всё интегрировать. Есть два пути интеграции в OPA:
  • Push-интеграция, когда через REST API помещаем наши проверки и внешние данные в Open Policy Agent:

curl -X PUT localhost:8181/v1/data/checks/ --data-binary @check_packages.rego
curl -X PUT localhost:8181/v1/data/checks/packages --data-binary @permitted_packages.json

  • Pull-интеграция OPA bundle server, которая мне очень нравится.

Представьте, у вас есть Open Policy Agent сервер, есть bundle-сервер (который надо написать самостоятельно) и есть данные по вашим проверкам:



В соответствии с конфигурацией Open Policy Agent сервер умеет ходить в bundle-сервер по фиксированному урлу и забирать данные, если они ещё не подготовлены. Далее он ожидает их приема в .gzip формате, полностью поддерживая спецификации кэширования если вы в bundle-сервере реализовали обработку тэга ETag, то Open Policy Agent сервер не будет протаскивать мегабайты ваших проверок через сеть в случае неизменения:



Выполняем проверку. Open Policy Agent имеет прекрасный resfull API после загрузки наших данных мы получаем имя пакета, который мы создали при создании файла, и имя правила. Дальше мы подаём для проверки наш артефакт в виде JSON. Если вы долго читали документацию, то поняли, что наш артефакт надо обрамить в input:



Обрабатываем результат (возвращённый JSON) где угодно в Jenkins, admission controller, UI, whatever Чаще всего результат приходит в таком виде:



Повторное и автоматическое использование проверок


Политики OPA у нас разработаны для тех проверок, которые не связаны с Kubernetes, и тех, что связаны с Kubernetes, но рекомендуются. Они хранятся в гите и через наш Bundle server, как и метаданные, попадают в Open Policy Agent. Мы пользуемся встроенными механизмами Open Policy Agent для Unit-тестирования и тестируем все наши проверки.

То есть Open Policy Agent проверяет сторонние артефакты (Java configs, K8S config, артефакты CI/CD), а те, которые надо заблокировать для попадания на кластеры, проходят через Gatekeeper, и в итоге наш кластер в безопасности. В принципе мы можем проверять всё что угодно, и смотреть результаты проверок через наш пользовательский интерфейс:



Чего мы добились:
  • У нас есть 24 обязательных правила и 12 необязательных;
  • 80 правил планируется к использованию в данном продукте;
  • Автоматическая проверка одного проекта за 10-20 секунд.

Конечно, стоит подумать об Opensource Bundle-сервера и пользовательского интерфейса, потому что это дженерик-решение, и они никак не связаны с инфраструктурой Сбербанка.

Коррекции на основе проверок


Приведу пример банка Goldman Sachs. У них большой облачный кластер из 1800 namespace (12 общих кластера Kubernetes на виртуалках, в каждом кластере по 150 namespace). Всё это управляется из централизованной системы под названием Inventory, где находится информация:
  • По безопасности (Security inventory): Roles, Rolebindings, Clusterroles, Clusterrolbindings;
  • По управлению ресурсами (Capacity Inventory): cpu, memory через ResourceQuotas и LimitRange;
  • По управлению дисками (NFS inventory): Persistent volumes и Persistent volume claims.

Коллеги используют такую архитектуру решения:
  • Pull измененных политик и данных из гита и inventory через bundle-сервер;
  • Создание объектов в K8S на основе данных, создаваемых OPA;
  • Hand-made mutating admission controller на основе JSON, возвращенного OPA, формируется YAML и прогоняется на кластере:

Всё состояние кластера синхронизируются в Bundle server с помощью плагина Kube-mgmt от разработчиков OPA. Bundle server через определенные периоды времени ходит в гит и забирает измененные политики.
Для самой системы Inventory процесс чуть-чуть другой. Она уведомляет Bundle server об изменениях, и он сразу же после этого идёт и забирает данные. Любое изменение вызывает запуск проверок, который вызывает создание JSON, а JSON переводится в YAML, прогоняется через Controller Manager и запускается на кластере.



Когда такая система стоит в продакшене, очень важно её мониторить. Коллеги мониторят 11 показателей, относящихся по большей части к garbage коллектору, Go и времени ответа от OPA-сервера:
  1. Go routine and thread counts;
  2. Memory in use (stack vs heap);
  3. Memory allocated (stack vs heap);
  4. GC stats;
  5. Pointer lookup count;
  6. Roundtrip time by http method;
  7. Percentage of requests under 500ms, 200ms, 50ms;
  8. Mean API request latency;
  9. Recommendations for alerting;
  10. Number of OPA instances up at any given time;
  11. OPA responding under 200ms for 95% of requests.

Что же удалось создать коллегам:
  • 24 проверки;
  • 1 Mb справочных данных по безопасности 3500 правил;
  • 2 Mb справочных данных по дискам и ресурсам 8000 правил;
  • Применение правила на кластере от 2 до 5 минут реально очень быстро.


Продвинутые проверки


Если мы хотим писать крутые, навороченные проверки, что же делать? Есть несколько способов взять готовое или стать ещё более крутым.

Fugue


Мы можем использовать Fugue, который представляет движок политик как сервис (governance as a code as a service). Он проверяет конфигурации облаков Amazon, Azure, GCP. Представляете, насколько там сложные политики, что ему надо проверять не только Kubernetes, но и relational database services, VPC и т.д.? Фактически каждый из продуктов каталога Amazon и прочих облаков может быть проверен готовыми политиками.
Fugue работает как admission controller. Ещё одна из очень его больших ценностей это наборы пресетов под регуляторы PSI DSS, HIPAA, SOC, etc. Фактически вы делаете нужную инфраструктуру в Amazon, запускаете продукт и говорите: Проверь на PSI DSS, и вуаля, вы получаете фактически аудит.

Очевидно, что Fugue реализован на OPA, но так как политики очень сложные, коллеги реализовали свой интерпретатор Rego для отладки продвинутых проверок. Выполнение происходит на Open Policy Agent, а отладка уже на своем интерпретаторе. Так как это облачный продукт, у него прекраснейший пользовательский интерфейс, который позволяет понять, какие проверки пройдены, какие нет и процент их соответствия:



Набор правил действительно самый разнообразный есть элементарнейшие вещи в стиле на машине с RDS, если она гарантирует высокую доступность, должны быть использованы multi availability zone deployment:



И есть гораздо более сложные, например, если используется Elasticsearch от Amazon, то его пользовательский интерфейс не должен быть выставлен наружу:



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

Fregot


Fregot это очень живой продукт, который позволяет отлаживать проверки на Rego. За счет чего? У него упрощённый дебаг (breakpoints и watch variables) и расширенная диагностика по ошибкам, которая важна, когда вы будете пользоваться ванильной OPA в этом случае чаще всего вы будете получать сообщение: var _ is unsafe, и вам будет абсолютно непонятно, что с этим делать:



Conftest


Это утилита проверки, использующая Open Policy Agent. Она содержит в себе потрясающе огромный встроенный набор конвертеров, и сама выбирает конвертер, исходя из расширения файла:



Ещё раз обратите внимание, насколько выразительный язык Rego всего одной строкой мы проверяем, что нельзя запускать контейнер из-под root:



Дальше мы говорим: conftest test, и указываем имя того объекта, который нам надо протестировать. И все проверки, которые есть в директории утилиты, проверяются по файлам, а вы получаете результат.

Плюсы:
  • Большое количество конверторов файлов: YAML, INI, TOML, HOCON, HCL, HCL1, CUE, Dockerfile, EDN, VCL, XML.
  • Много примеров проверок при работе с Open Policy Agent.

Минусы:
  • Не могу сказать, что утилита стабильно работает
  • Так как эта утилита командной строки, то она однопоточная, на продакшн ее не так круто применять.


Gatekeeper


Сейчас это Admission controller, а в будущем он будет работать как Mutating admission controller, который не только проверяет на условие, но и в случае прохождения условий меняет команду так, как ему надо:



Admission controller встает между вашими командами и кластером Kubernetes. Вы отправляете команду, admission controller вызывает внешнюю проверку условий и, если условие выполнено, то команда запускается в кластере. Если не выполнено, то команда просто откатывается.

За счет чего обеспечивается повторное использование в продукте Gatekeeper? Есть два объекта:
  • Policy Template это механизм описания функций, где вы задаете текст проверки на Rego и набор входных параметров;
  • Сама политика это вызов функции с указанием значений параметров.

Gatekeeper синхронизирует в течение жизненного цикла своей работы весь кластер кубера в себя, а дальше любое действие кластера вызывает Webhook, по нему команда (YAML объект, каким он будет после применения изменения) приходит в Open Policy Agent. Дальше результат проверки возвращается как пропущенная, либо как нет.

Пример проверки на Gatekeeper:


Это своя CRD, само собой. Мы даем ей имя и говорим, что входной параметр это метки, которые представляют собой массив строк. Пишем на Rego нашу проверку, и в данном случае она проверяет, что для объектов заданы необходимые метки. Мы используем внутри Rego наши входные параметры это тот самый template. А живая политика выглядит так: используй такой-то template и такие-то метки. Если у проекта не будет таких меток, то он просто в кластер не попадёт:



Плюсы:
  • Великолепно работает повторное использование через template;
  • Есть огромные готовые библиотеки проверок;
  • Тесная интеграция с K8S.

В то же время эти плюсы становятся автоматически минусами:
  • Нельзя запустить Gatekeeper без K8S;
  • Нет юнит-тестов констрейнтов и темплейтов;
  • Многословные CRD;
  • Нет UI (пользовательского интерфейса);
  • информацию для аналитики приходится получать через парсинг логов;
  • Закрыта возможность вызова внешних сервисов;
  • Нельзя использовать bundle server для помещения в него внешних данных;
  • Функционал Mutation в процессе разработки, о чем регулярно напоминают разработчикам в их гите.

Что мы получили на выходе


Итак, фактически мы преобразовали уровни зрелости технологического контроля к готовому технологическому стеку:



OPA на практике use cases


Как мы увидели, Open Policy Agent решает задачи проверки любых структурированных данных, а также коррекции проверенных данных. Хотя на самом деле Open Policy Agent позволяет делать гораздо больше, например, помогает решать задачи авторизации, задачи database row level security (sql databases, ElasticSearch), а некоторые даже пишут на нём даже игры (Corrupting the Open Policy Agent to Run My Game).

Рассмотрим несколько примеров.

OPA as a sidecar


У коллег в Pinterest всё развернуто в Amazon, поэтому у них принят подход Zero-trust security и, само собой, все реализовано на OPA. Под его контролем находится всё, что связано с Kubernetes, с виртуальными машинами, авторизацией в Kafka, а также с авторизацией на Envoy.

Таким образом их нагрузка колеблется от 4.1M до 8.5M QPS в секунду. При этом имеется кэш с длительностью жизни 5 минут. В Open Policy Agent приходит от 204 до 437 тысяч в секунду запросов действительно большие цифры. Если вы хотите работать с такими порядками, надо конечно думать о производительности.

Что я рекомендую в этой части:
  • Network footprint подумать о тех нюансах, которые приносит сеть.
  • OPA library single-thread если вы используете Open Policy Agent не как сервер, а как библиотеку, то библиотека будет однопоточной.
  • Use OPA server instead multi-thread вам придется делать многопоточность вручную.
  • Memory for data 20x from raw data если у вас есть 10 МБ внешних данных каких-нибудь JSON-справочников, то они превратятся в 200 МБ на сервере.
  • Partial evaluation ms to ns вам надо разобраться, как работает его фишка механизма предрасчета, фактически компилирование статической части проверок.
  • Memory for partial evaluation cache вам надо понимать, что для всего этого требуются кэши.
  • Beware arrays с массивами Open Policy Agent работает не так хорошо.
  • Use objects instead и именно поэтому коллеги с Amazon транслировали массивы в объекты.

Как же коллеги из Pinterest решили эти задачи? На каждом сервисе или каждом хосте, или ноде у них расположен Open Policy Agent как sidecar, и работают они с ним через библиотеку, а не напрямую через REST, как показывал я. В той самой библиотеке находится кэш с временем жизни 5 минут:



Очень интересно, как происходит у них доставка политик. Есть кластер, есть Bitbucket, а дальше данные из него хуками на коммит пробрасываются на S3. Так как запись в него асинхронна в принципе, то результат идет в Zookeeper, а он уведомляет Sidecar об изменениях. Sidecar забирает данные из S3, и всё прекрасно:



OPA authorization


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



Но можно это сделать по-другому. Наверное, все знают, что такое Envoy. Это легковесный прокси, в котором есть цепочки фильтров. Envoy крут тем, что позволяет обеспечивать дистанционное управление конфигурацией. Собственно, отсюда и появился термин service mesh:



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



Penetration testing


На самом деле авторизация это всегда о безопасности. Поэтому в 2018 году было проведено Penetration-тестирование: 6 человек в течение 18 дней пытались взломать Open Policy Agent. Результаты тестирования говорят, что OPA это действительно безопасное решение:



Конечно, были ошибки:
Identified Vulnerabilities
OPA-01-001 Server: Insecure Default Config allows to bypass Policies (Medium)
OPA-01-005 Server: OPA Query Interface is vulnerable to XSS (High)
Miscellaneous Issues
OPA-01-002 Server: Query Interface can be abused for SSRF (Medium)
OPA-01-003 Server: Unintended Behavior due to unclear Documentation (Medium)
OPA-01-004 Server: Denial of Service via GZip Bomb in Bundle (Info)
OPA-01-006 Server: Path Mismatching via HTTP Redirects (Info) Conclusions Introduct

Но это были те ошибки, которые случаются по большей части от того, что есть проблемы с документацией:
What is more, the shared documentation was unclear and misleading at times (see OPA-01-001), so that arriving at a secure configuration and integration would require a user to have an extensive and nearly-internal-level of knowledge. As people normally cannot be expected to know what to look for, this poses a risk of insecure configurations.


OPA не экзотика


Из того, что я сказал, может создаться ощущение, что Open Policy Agent это странный, непонятный, никому не нужный продукт. На самом деле в Open Policy Agent полно фишек, которые еще никто не использует:
  • Юнит-тестирование;
  • Трейсинг, профайлинг и бенчмаркинг;
  • Механизмы ускорения производительности Conditional evaluation;
  • Возможность обращения к внешним http-сервисам;
  • Работа с JWT-токенами.

Есть только одна проблема документация не объясняет, зачем это надо. В Google, скорее всего, вы тоже ничего не найдете. Например, мы хотим понять, как же работает политика изнутри:



Идем по первой же ссылке и попадаем всего лишь в функцию, которая выводит отладочную информацию, не имеющую никакого отношения к тому, как же политика выполняется:



Но если поковыряться с утилитой repl, которая входит в состав OPA, абсолютно случайно можно найти функцию trace, про которую, конечно же, ничего не сказано в документации. Именно она фактически показывает план выполнения проверки, с которым очень легко работать и отлаживаться:



Open Policy Agent находится под крылом CNCF, и он более чем живой. В итоге хотелось бы сказать, что да, по данному продукту сложно найти информацию даже в объемной и неоднозначной документации. Но продукт активно развивается, живет не только в контейнерах, его можно использовать в абсолютно разных кейсах. И он действительно безопасен, как показали Penetration-тесты.

Если вы хотите его использовать, вам придется создавать UI и научиться писать правила на языке, близком к естественному, а не на языке кода. Учите Rego и используйте Fregot для дебага.

Помните, что Гугл, скорее всего, вам не поможет.

Тем временем мы готовимся к конференции HighLoad++, которая состоится офлайн 9 и 10 ноября в Москве (Сколково). Это будет наша первая очная встреча после 9 месяцев онлайнового общения.

HighLoad++ это 3000 участников, 160 докладов, 16 параллельных треков докладов, мастер-классов и митапов. Единственная конференция, где за два дня можно узнать, как устроены Facebook, ВКонтакте, Одноклассники, Яндекс, Mail.ru, Amazon, Badoo, Авито, Alibaba и другие крупнейшие компании.



А в этом году главная задача HighLoad++ выяснить пределы технологий. Как обычно, каждый доклад будет о решении конкретной задачи.Если есть что-то достойное в мире технологий, то это точно будет на HighLoad++ :)

Новости и подборки докладов мы публикуем в рассылке и в telegram-канале @HighLoadChannel подпишитесь, чтобы быть в курсе обновлений.
Подробнее..

Перевод Паттерн проектирования Builder (Строитель) в Java

14.04.2021 20:10:55 | Автор: admin

В преддверии скорого старта курса Архитектура и шаблоны проектирования делимся с вами переводом материала.

Приглашаем также всех желающих на открытый демо-урок
Шаблоны GRASP. На этом занятии мы проанализируем функциональное разделение функционала и рассмотрим 9 шаблонов GRASP. Присоединяйтесь!


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

Паттерн проектирования Builder

  • Паттерн проектирования Builder разработан для обеспечения гибкого решения различных задач создания объектов в объектно-ориентированном программировании.

  • Паттерн проектирования Builder позволяет отделить построение сложного объекта от его представления.

  • Паттерн Builder создает сложные объекты, используя простые объекты и поэтапный подход.

  • Паттерн предоставляет один из лучших способов создания сложных объектов.

  • Этот паттерн полезен для создания разных иммутабельных объектов с помощью одного и того же процесса построения объекта.

Паттерн Builder это паттерн проектирования, который позволяет поэтапно создавать сложные объекты с помощью четко определенной последовательности действий. Строительство контролируется объектом-распорядителем (director), которому нужно знать только тип создаваемого объекта.

Итак, паттерн проектирования Builder можно разбить на следующие важные компоненты:

  • Product (продукт) - Класс, который определяет сложный объект, который мы пытаемся шаг за шагом сконструировать, используя простые объекты.

  • Builder (строитель) - абстрактный класс/интерфейс, который определяет все этапы, необходимые для производства сложного объекта-продукта. Как правило, здесь объявляются (абстрактно) все этапы (buildPart), а их реализация относится к классам конкретных строителей (ConcreteBuilder).

  • ConcreteBuilder (конкретный строитель) - класс-строитель, который предоставляет фактический код для создания объекта-продукта. У нас может быть несколько разных ConcreteBuilder-классов, каждый из которых реализует различную разновидность или способ создания объекта-продукта.

  • Director (распорядитель) - супервизионный класс, под конролем котрого строитель выполняет скоординированные этапы для создания объекта-продукта. Распорядитель обычно получает на вход строителя с этапами на выполнение в четком порядке для построения объекта-продукта.

Паттерн проектирования Builder решает такие проблемы, как:

  • Как класс (тот же самый процесс строительства) может создавать различные представления сложного объекта?

  • Как можно упростить класс, занимающийся созданием сложного объекта?

Давайте реализуем пример со сборкой автомобилей, используя паттерн проектирования Builder.

Пример со сборкой автомобилей с использованием паттерна проектирования Builder

Шаг 1: Создайте класс Car (автомобиль), который в нашем примере является продуктом:

package org.trishinfotech.builder;public class Car {    private String chassis;    private String body;    private String paint;    private String interior;        public Car() {        super();    }    public Car(String chassis, String body, String paint, String interior) {        this();        this.chassis = chassis;        this.body = body;        this.paint = paint;        this.interior = interior;    }    public String getChassis() {        return chassis;    }public void setChassis(String chassis) {        this.chassis = chassis;    }    public String getBody() {        return body;    }    public void setBody(String body) {        this.body = body;    }    public String getPaint() {        return paint;    }    public void setPaint(String paint) {        this.paint = paint;    }public String getInterior() {        return interior;    }    public void setInterior(String interior) {        this.interior = interior;    }    public boolean doQualityCheck() {        return (chassis != null && !chassis.trim().isEmpty()) && (body != null && !body.trim().isEmpty())                && (paint != null && !paint.trim().isEmpty()) && (interior != null && !interior.trim().isEmpty());    }    @Override    public String toString() {        // StringBuilder class also uses Builder Design Pattern with implementation of java.lang.Appendable interface        StringBuilder builder = new StringBuilder();        builder.append("Car [chassis=").append(chassis).append(", body=").append(body).append(", paint=").append(paint)        return builder.toString();    }}

Обратите внимание, что я добавил в класс проверочный метод doQualityCheck. Я считаю, что Builder не должен создавать неполные или невалидные Product-объекты. Таким образом, этот метод поможет нам в проверке сборки автомобилей.

Шаг 2: Создайте абстрактный класс/интерфейс CarBuilder, в котором определите все необходимые шаги для создания автомобиля.

package org.trishinfotech.builder;public interface CarBuilder {    // Этап 1    public CarBuilder fixChassis();    // Этап 2    public CarBuilder fixBody();    // Этап 3    public CarBuilder paint();    // Этап 4    public CarBuilder fixInterior();    // Выпуск автомобиля    public Car build();}

Обратите внимание, что я сделал тип CarBuilder типом возврата всех этапов, созданных здесь. Это позволит нам вызывать этапы по цепочке. Здесь есть один очень важный метод build, который заключается в том, чтобы получить результат или создать конечный объект Car. Этот метод фактически проверяет годность автомобиля и выпускает (возвращает) его только в том случае, если его сборка завершена успешно (все валидно).

Шаг 3: Теперь пора написать ConcreteBuilder. Как я уже упоминал, у нас могут быть разные варианты ConcreteBuilder, и каждый из них выполняет сборку по-своему, чтобы предоставить нам различные представления сложного объекта Car.

Итак, ниже приведен код ClassicCarBuilder, который собирает старые модели автомобилей.

package org.trishinfotech.builder;public class ClassicCarBuilder implements CarBuilder {    private String chassis;    private String body;    private String paint;    private String interior;    public ClassicCarBuilder() {        super();    }    @Override    public CarBuilder fixChassis() {        System.out.println("Assembling chassis of the classical model");        this.chassis = "Classic Chassis";        return this;    }    @Override    public CarBuilder fixBody() {        System.out.println("Assembling body of the classical model");        this.body = "Classic Body";        return this;    }    @Override    public CarBuilder paint() {        System.out.println("Painting body of the classical model");        this.paint = "Classic White Paint";        return this;    }      @Override    public CarBuilder fixInterior() {        System.out.println("Setting up interior of the classical model");        this.interior = "Classic interior";        return this;    }    @Overridepublic Car build() {        Car car = new Car(chassis, body, paint, interior);        if (car.doQualityCheck()) {            return car;        } else {            System.out.println("Car assembly is incomplete. Can't deliver!");        }        return null;    }}

Теперь напишем еще один строитель ModernCarBuilder для сборки последней модели автомобиля.

package org.trishinfotech.builder;public class ModernCarBuilder implements CarBuilder {    private String chassis;    private String body;    private String paint;    private String interior;    public ModernCarBuilder() {        super();    }    @Override    public CarBuilder fixChassis() {        System.out.println("Assembling chassis of the modern model");        this.chassis = "Modern Chassis";        return this;    }    @Override    public CarBuilder fixBody() {        System.out.println("Assembling body of the modern model");        this.body = "Modern Body";        return this;    }      @Override    public CarBuilder paint() {        System.out.println("Painting body of the modern model");        this.paint = "Modern Black Paint";        return this;    }    @Override    public CarBuilder fixInterior() {        System.out.println("Setting up interior of the modern model");        this.interior = "Modern interior";        return this;    }    @Override    public Car build() {        Car car = new Car(chassis, body, paint, interior);        if (car.doQualityCheck()) {            return car;        } else {            System.out.println("Car assembly is incomplete. Can't deliver!");        }        return null;    }}

И еще один SportsCarBuilder для создания спортивного автомобиля.

package org.trishinfotech.builder;public class SportsCarBuilder implements CarBuilder {    private String chassis;    private String body;    private String paint;    private String interior;    public SportsCarBuilder() {        super();    }    @Override    public CarBuilder fixChassis() {        System.out.println("Assembling chassis of the sports model");        this.chassis = "Sporty Chassis";        return this;    }     @Override    public CarBuilder fixBody() {        System.out.println("Assembling body of the sports model");        this.body = "Sporty Body";        return this;    }      @Override    public CarBuilder paint() {        System.out.println("Painting body of the sports model");        this.paint = "Sporty Torch Red Paint";        return this;    }    @Override    public CarBuilder fixInterior() {        System.out.println("Setting up interior of the sports model");        this.interior = "Sporty interior";        return this;    }    @Override    public Car build() {        Car car = new Car(chassis, body, paint, interior);        if (car.doQualityCheck()) {            return car;        } else {            System.out.println("Car assembly is incomplete. Can't deliver!");        }        return null;    }}

Шаг 4: Теперь мы напишем класс-распорядитель AutomotiveEngineer, под руководством которого строитель будет собирать автомобиль (объект Car) шаг за шагом в четко определенном порядке.

package org.trishinfotech.builder;public class AutomotiveEngineer {    private CarBuilder builder;    public AutomotiveEngineer(CarBuilder builder) {        super();        this.builder = builder;        if (this.builder == null) {            throw new IllegalArgumentException("Automotive Engineer can't work without Car Builder!");        }    }    public Car manufactureCar() {        return builder.fixChassis().fixBody().paint().fixInterior().build();    }}

Мы видим, что метод manufactureCar вызывает этапы сборки автомобиля в правильном порядке.

Теперь пришло время написать класс Main для выполнения и тестирования нашего кода.

package org.trishinfotech.builder;public class Main {    public static void main(String[] args) {        CarBuilder builder = new SportsCarBuilder();        AutomotiveEngineer engineer = new AutomotiveEngineer(builder);        Car car = engineer.manufactureCar();        if (car != null) {            System.out.println("Below car delievered: ");            System.out.println("======================================================================");            System.out.println(car);            System.out.println("======================================================================");        }    }}

Ниже приведен вывод программы:

Assembling chassis of the sports modelAssembling body of the sports modelPainting body of the sports modelSetting up interior of the sports modelBelow car delievered: ======================================================================Car [chassis=Sporty Chassis, body=Sporty Body, paint=Sporty Torch Red Paint, interior=Sporty interior]======================================================================

Я надеюсь, что вы хорошо разобрались в объяснении и примере, чтобы понять паттерн Builder. Некоторые из нас также находят у него сходство с паттерном абстрактной фабрики (Abstract Factory), о котором я рассказывал в другой статье. Основное различие между строителем и абстрактной фабрикой состоит в том, что строитель предоставляет нам больший или лучший контроль над процессом создания объекта. Если вкратце, то паттерн абстрактной фабрики отвечает на вопрос что, а паттерн строитель - как.

Исходный код можно найти здесь: Real-Builder-Design-Pattern-Source-Code

Я нашел паттерн Builder невероятно полезным и одним из наиболее часто используемых в приложениях в настоящее время. Я пришел к выводу, что Builder лучше подходит для работы с иммутабельными объектами. Все мы знаем, как много есть хороших иммутабельных объектов, и их использование увеличивается день ото дня, особенно после релиза Java 8.

Я использую Builder для написания своих сложных иммутабельных классов, и я бы хотел продемонстриовать здесь эту идею.

В качестве примера у нас есть класс Employee, в котором есть несколько полей.

public class Employee {    private int empNo;    private String name;    private String depttName;    private int salary;    private int mgrEmpNo;    private String projectName;}

Предположим, только два поля EmpNo и EmpName являются обязательными, а все остальные - опциональные. Поскольку это иммутабельный класс, у меня есть два варианта написания конструкторов.

  1. Написать конструктор с параметрами под все поля.

  2. Написать несколько конструкторов для разных комбинаций параметров, чтобы создать разные представления объекта Employee.

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

Employee emp1 = new Employee (100, "Brijesh", null, 0, 0, "Builder Pattern");

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

  public Employee(int empNo, String name) {        super();        if (empNo <= 0) {            throw new IllegalArgumentException("Please provide valid employee number.");        }        if (name == null || name.trim().isEmpty()) {            throw new IllegalArgumentException("Please provide employee name.");        }        this.empNo = empNo;        this.name = name;    }    public Employee(int empNo, String name, String depttName) {        this(empNo, name);        this.depttName = depttName;    }    public Employee(int empNo, String name, String depttName, int salary) {        this(empNo, name, depttName);        this.salary = salary;    }    public Employee(int empNo, String name, String depttName, int salary, int mgrEmpNo) {        this(empNo, name, depttName, salary);        this.mgrEmpNo = mgrEmpNo;    }    public Employee(int empNo, String name, String depttName, int salary, int mgrEmpNo, String projectName) {        this(empNo, name, depttName, salary, mgrEmpNo);        this.projectName = projectName;    }

Итак, вот решение с помощью паттерна Builder:

package org.trishinfotech.builder.example;public class Employee {    private int empNo;    private String name;    private String depttName;    private int salary;    private int mgrEmpNo;    private String projectName;    public Employee(EmployeeBuilder employeeBuilder) {        if (employeeBuilder == null) {            throw new IllegalArgumentException("Please provide employee builder to build employee object.");        }        if (employeeBuilder.empNo <= 0) {            throw new IllegalArgumentException("Please provide valid employee number.");        }        if (employeeBuilder.name == null || employeeBuilder.name.trim().isEmpty()) {            throw new IllegalArgumentException("Please provide employee name.");        }        this.empNo = employeeBuilder.empNo;        this.name = employeeBuilder.name;        this.depttName = employeeBuilder.depttName;        this.salary = employeeBuilder.salary;        this.mgrEmpNo = employeeBuilder.mgrEmpNo;        this.projectName = employeeBuilder.projectName;    }    public int getEmpNo() {        return empNo;    }    public String getName() {        return name;    }        public String getDepttName() {        return depttName;    }    public int getSalary() {        return salary;    }    public int getMgrEmpNo() {        return mgrEmpNo;    }        public String getProjectName() {        return projectName;    }    @Override    public String toString() {        // Класс StringBuilder также использует паттерн проектирования Builder с реализацией        // интерфейса java.lang.Appendable        StringBuilder builder = new StringBuilder();        builder.append("Employee [empNo=").append(empNo).append(", name=").append(name).append(", depttName=")                .append(depttName).append(", salary=").append(salary).append(", mgrEmpNo=").append(mgrEmpNo)                .append(", projectName=").append(projectName).append("]");        return builder.toString();    }    public static class EmployeeBuilder {        private int empNo;        protected String name;        protected String depttName;        protected int salary;        protected int mgrEmpNo;        protected String projectName;        public EmployeeBuilder() {            super();        }                public EmployeeBuilder empNo(int empNo) {            this.empNo = empNo;            return this;        }        public EmployeeBuilder name(String name) {            this.name = name;            return this;        }        public EmployeeBuilder depttName(String depttName) {            this.depttName = depttName;            return this;        }        public EmployeeBuilder salary(int salary) {            this.salary = salary;            return this;        }        public EmployeeBuilder mgrEmpNo(int mgrEmpNo) {            this.mgrEmpNo = mgrEmpNo;            return this;        }        public EmployeeBuilder projectName(String projectName) {            this.projectName = projectName;            return this;        }        public Employee build() {            Employee emp = null;            if (validateEmployee()) {                emp = new Employee(this);            } else {                System.out.println("Sorry! Employee objects can't be build without required details");            }            return emp;        }        private boolean validateEmployee() {           return (empNo > 0 && name != null && !name.trim().isEmpty());        }    }}

Я написал EmployeeBuilder как публичный статический вложенный класс. Вы можете написать его как обычный публичный класс в отдельном файл Java. Большой разницы я не вижу.

Теперь напишем программу EmployeeMain для создания объекта Employee:

package org.trishinfotech.builder.example;public class EmployeeMain {    public static void main(String[] args) {        Employee emp1 = new Employee.EmployeeBuilder().empNo(100).name("Brijesh").projectName("Builder Pattern")                .build();        System.out.println(emp1);    }}

Надеюсь, вам понравилась идея. Мы можем использовать это при создании более сложных объектов. Я не реализовал здесь распорядителя (Director), так как все шаги (сбор значений для полей) не являются обязательными и могут выполняться в любом порядке. Чтобы убедиться, что я создаю объект Employee только после получения всех обязательных полей, я написал метод проверки.

Пример с оформлением заказа в ресторане с использованием паттерна Builder

Я хочу еще показать вам пример кода для оформления заказа в ресторане, где Order (заказ) является иммутабельным объектом и требует тип обслуживания заказа - Order Service Type (Take Away - с собой/Eat Here - в заведении), всех необходимых нам продуктов питания (Food Items) и имени клиента (Customer Name - опционально) в время оформления заказа. Продуктов питания может быть сколько угодно. Итак, вот код этого примера.

Код для перечисления OrderService:

package org.trishinfotech.builder;public enum OrderService {    TAKE_AWAY("Take Away", 2.0d), EAT_HERE("Eat Here", 5.5d);    private String name;    private double tax;    OrderService(String name, double tax) {        this.name = name;        this.tax = tax;    }    public String getName() {        return name;    }    public double getTax() {        return tax;    }}

Код для интерфейса FoodItem:

package org.trishinfotech.builder.meal;import org.trishinfotech.builder.packing.Packing;public interface FoodItem {    public String name();    public int calories();    public Packing packing();    public double price();}

Код для класса Meal (блюдо). Класс Meal предлагает заранее определенные продукты питания со скидкой на цену товара (не на цену упаковки).

package org.trishinfotech.builder.meal;import java.util.ArrayList;import java.util.List;import java.util.Objects;import java.util.stream.Collectors;import org.trishinfotech.builder.packing.MultiPack;import org.trishinfotech.builder.packing.Packing;public class Meal implements FoodItem {    private List<FoodItem> foodItems = new ArrayList<FoodItem>();    private String mealName;    private double discount;    public Meal(String mealName, List<FoodItem> foodItems, double discount) {        super();        if (Objects.isNull(foodItems) || foodItems.stream().filter(Objects::nonNull).collect(Collectors.toList()).isEmpty()) {            throw new IllegalArgumentException(                    "Meal can't be order without any food item");        }        this.mealName = mealName;        this.foodItems = new ArrayList<FoodItem>(foodItems);        this.discount = discount;    }    public List<FoodItem> getFoodItems() {        return foodItems;    }    @Override    public String name() {        return mealName;    }      @Override    public int calories() {        int totalCalories = foodItems.stream().mapToInt(foodItem -> foodItem.calories()).sum();        return totalCalories;    }    @Override    public Packing packing() {        double packingPrice = foodItems.stream().map(foodItem -> foodItem.packing())                .mapToDouble(packing -> packing.packingPrice()).sum();        return new MultiPack(packingPrice);    }    @Override    public double price() {        double totalPrice = foodItems.stream().mapToDouble(foodItem -> foodItem.price()).sum();        return totalPrice;    }    public double discount() {        return discount;    }}

Еда:

Код для класса Burger:

package org.trishinfotech.builder.food.burger;import org.trishinfotech.builder.meal.FoodItem;import org.trishinfotech.builder.packing.Packing;import org.trishinfotech.builder.packing.Wrap;public abstract class Burger implements FoodItem {    @Override    public Packing packing() {        return new Wrap();    }}

Код для класса ChickenBurger:

package org.trishinfotech.builder.food.burger;public class ChickenBurger extends Burger {    @Override    public String name() {        return "Chicken Burger";    }    @Override    public int calories() {        return 300;    }    @Override    public double price() {        return 4.5d;    }}

Код для класса VegBurger (веганский бургер):

package org.trishinfotech.builder.food.burger;public class VegBurger extends Burger {    @Override    public String name() {        return "Veg Burger";    }    @Override    public int calories() {        return 180;    }    @Override    public double price() {        return 2.7d;    }}

Код для класса Nuggets:

package org.trishinfotech.builder.food.nuggets;import org.trishinfotech.builder.meal.FoodItem;import org.trishinfotech.builder.packing.Container;import org.trishinfotech.builder.packing.Packing;public abstract class Nuggets implements FoodItem {    @Override    public Packing packing() {        return new Container();    }}

Код для класса CheeseNuggets:

package org.trishinfotech.builder.food.nuggets;public class CheeseNuggets extends Nuggets {    @Override    public String name() {        return "Cheese Nuggets";    }    @Override    public int calories() {        return 330;    }    @Override    public double price() {        return 3.8d;    }}

Код для класса ChickenNuggets:

package org.trishinfotech.builder.food.nuggets;public class ChickenNuggets extends Nuggets {    @Override    public String name() {        return "Chicken Nuggets";    }    @Override    public int calories() {        return 450;    }    @Override    public double price() {        return 5.0d;    }}

Напитки:

Напитки бывают разных размеров. Итак, вот код перечисления BeverageSize:

package org.trishinfotech.builder.beverages;public enum BeverageSize {    XS("Extra Small", 110), S("Small", 150), M("Medium", 210), L("Large", 290);    private String name;    private int calories;    BeverageSize(String name, int calories) {        this.name = name;        this.calories = calories;    }      public String getName() {        return name;    }    public int getCalories() {        return calories;    }}

Код для класса Drink:

package org.trishinfotech.builder.beverages;import org.trishinfotech.builder.meal.FoodItem;public abstract class Drink implements FoodItem {    protected BeverageSize size;    public Drink(BeverageSize size) {        super();        this.size = size;        if (this.size == null) {            this.size = BeverageSize.M;        }    }    public BeverageSize getSize() {        return size;    }    public String drinkDetails() {        return " (" + size + ")";    }}

Код для класса ColdDrink:

package org.trishinfotech.builder.beverages.cold;import org.trishinfotech.builder.beverages.BeverageSize;import org.trishinfotech.builder.beverages.Drink;import org.trishinfotech.builder.packing.Bottle;import org.trishinfotech.builder.packing.Packing;public abstract class ColdDrink extends Drink {    public ColdDrink(BeverageSize size) {        super(size);    }    @Override public Packing packing() {        return new Bottle();    }}

Код для класса CocaCola:

package org.trishinfotech.builder.beverages.cold;import org.trishinfotech.builder.beverages.BeverageSize;public class CocaCola extends ColdDrink {    public CocaCola(BeverageSize size) {        super(size);    }    @Override    public String name() {        return "Coca-Cola" + drinkDetails();    }    @Override    public int calories() {        if (size != null) {            switch (size) {            case XS:                return 110;            case S:                return 150;            case M:                return 210;            case L:                return 290;            default:                break;            }        }        return 0;    }    @Override    public double price() {        if (size != null) {            switch (size) {            case XS:                return 0.80d;            case S:                return 1.0d;            case M:                return 1.5d;            case L:                return 2.0d;            default:                break;            }        }        return 0.0d;    }}

Код для класса Pepsi:

package org.trishinfotech.builder.beverages.cold;import org.trishinfotech.builder.beverages.BeverageSize;public class Pepsi extends ColdDrink {    public Pepsi(BeverageSize size) {        super(size);    }    @Override public String name() {        return "Pepsi" + drinkDetails();    }    @Override public int calories() {        if (size != null) {            switch (size) {                case S:                    return 160;                case M:                    return 220;                case L:                    return 300;                default:                    break;            }        }        return 0;    }    @Override public double price() {        if (size != null) {            switch (size) {                case S:                    return 1.2d;                case M:                    return 2.2d;                case L:                    return 2.7d;                default:                    break;            }        }        return 0.0d;    }}

Код для класса HotDrink:

package org.trishinfotech.builder.beverages.hot;import org.trishinfotech.builder.beverages.BeverageSize;import org.trishinfotech.builder.beverages.Drink;import org.trishinfotech.builder.packing.Packing;import org.trishinfotech.builder.packing.SipperMug;public abstract class HotDrink extends Drink {    public HotDrink(BeverageSize size) {        super(size);    }        @Override public Packing packing() {        return new SipperMug();    }}

Код для класса Cuppuccinno:

package org.trishinfotech.builder.beverages.hot;import org.trishinfotech.builder.beverages.BeverageSize;public class Cappuccino extends HotDrink {    public Cappuccino(BeverageSize size) {        super(size);    }    @Override public String name() {        return "Cappuccino" + drinkDetails();    }      @Override public int calories() {        if (size != null) {            switch (size) {                case S:                    return 120;                case M:                    return 160;                case L:                    return 210;                default:                break;            }        }        return 0;    }    @Override public double price() {        if (size != null) {            switch (size) {                case S:                    return 1.0d;                case M:                    return 1.4d;                case L:                    return 1.8d;                default:                break;            }        }        return 0.0d;    }}

Код для класса HotChocolate:

package org.trishinfotech.builder.beverages.hot;import org.trishinfotech.builder.beverages.BeverageSize;public class HotChocolate extends HotDrink {    public HotChocolate(BeverageSize size) {        super(size);    }    @Override public String name() {        return "Hot Chocolate" + drinkDetails();    }      @Override public int calories() {        if (size != null) {            switch (size) {                case S:                    return 370;                case M:                    return 450;                case L:                    return 560;                default:                    break;            }        }        return 0;    }        @Override public double price() {        if (size != null) {            switch (size) {                case S:                    return 1.6d;                case M:                    return 2.3d;                case L:                    return 3.0d;                default:                    break;            }                 }        return 0.0d;    }}

Упаковка:

Код интерфейса Packing:

package org.trishinfotech.builder.packing;public interface Packing {    public String pack();    public double packingPrice();}

Код для класса Bottle:

package org.trishinfotech.builder.packing;public class Bottle implements Packing {    @Override    public String pack() {        return "Bottle";    }    @Override    public double packingPrice() {        return 0.75d;    }}

Код для класса Container:

package org.trishinfotech.builder.packing;public class Container implements Packing {    @Override    public String pack() {        return "Container";    }    @Override    public double packingPrice() {        return 1.25d;    }}

Код для класса MultiPack. Упаковка MutiPack служит вспомогательной упаковкой для еды, когда мы используем разные упаковки для разных продуктов.

package org.trishinfotech.builder.packing;public class MultiPack implements Packing {    private double packingPrice;    public MultiPack(double packingPrice) {        super();        this.packingPrice = packingPrice;    }      @Override    public String pack() {        return "Multi-Pack";    }    @Override    public double packingPrice() {        return packingPrice;    }}

Код для класса SipperMug:

package org.trishinfotech.builder.packing;public class SipperMug implements Packing {    @Override    public String pack() {        return "Sipper Mug";    }    @Override    public double packingPrice() {        return 1.6d;    }}

Код для класса Wrap:

package org.trishinfotech.builder.packing;public class Wrap implements Packing {    @Override    public String pack() {        return "Wrap";    }    @Override    public double packingPrice() {        return 0.40d;    }}

Код служебного класса BillPrinter, который я написал для печати детализированного счета.

package org.trishinfotech.builder.util;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.concurrent.atomic.DoubleAdder;import org.trishinfotech.builder.Order;import org.trishinfotech.builder.OrderService;import org.trishinfotech.builder.meal.Meal;import org.trishinfotech.builder.packing.Packing;public class BillPrinter {    static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");    public static void printItemisedBill(Order order) {        OrderService service = order.getService();        System.out.printf("%60s\n", "Food Court");        System.out.println("=================================================================================================================");        System.out.printf("Service: %10s (%2.2f Tax)                                                         Customer Name: %-20s\n", service.getName(), service.getTax(), order.getCustomerName());        System.out.println("-----------------------------------------------------------------------------------------------------------------");        System.out.printf("%25s | %10s | %10s | %10s | %15s | %10s | %10s\n", "Food Item", "Calories", "Packing", "Price", "Packing Price", "Discount %", "Total Price");        System.out.println("-----------------------------------------------------------------------------------------------------------------");        DoubleAdder itemTotalPrice = new DoubleAdder();        order.getFoodItems().stream().forEach(item -> {            String name = item.name();            int calories = item.calories();            Packing packing = item.packing();            double price = item.price();            double packingPrice = packing.packingPrice();            double discount = item instanceof Meal? ((Meal)item).discount() : 0.0d;            double totalItemPrice = calculateTotalItemPrice(price, packingPrice, discount);            System.out.printf("%25s | %10d | %10s | %10.2f | %15.2f | %10.2f | %10.2f\n", name, calories, packing.pack(), price, packing.packingPrice(), discount, totalItemPrice);            itemTotalPrice.add(totalItemPrice);        });        System.out.println("=================================================================================================================");        double billTotal = itemTotalPrice.doubleValue();        billTotal = applyTaxes(billTotal, service);        System.out.printf("Date: %-30s %66s %.2f\n", dtf.format(LocalDateTime.now()), "Total Bill (incl. taxes):", billTotal);        System.out.println("Enjoy your meal!\n\n\n\n");    }    private static double applyTaxes(double billTotal, OrderService service) {        return billTotal + (billTotal * service.getTax())/100;    }    private static double calculateTotalItemPrice(double price, double packingPrice, double discount) {        if (discount > 0.0d) {            price = price - (price * discount)/100;        }        return price + packingPrice;    }}

Почти все готово. Пришло время написать наш иммутабельный класс Order:

package org.trishinfotech.builder;import java.util.ArrayList;import java.util.List;import java.util.Objects;import java.util.stream.Collectors;import org.trishinfotech.builder.meal.FoodItem;public class Order {    private List<FoodItem> foodItems = new ArrayList<FoodItem>();    private String customerName;    private OrderService service;    public Order(OrderService service, List<FoodItem> foodItems, String customerName) {        super();        if (Objects.isNull(service)) {            throw new IllegalArgumentException(                    "Meal can't be order without selecting service 'Take Away' or 'Eat Here'");        }        if (Objects.isNull(foodItems) || foodItems.stream().filter(Objects::nonNull).collect(Collectors.toList()).isEmpty()) {            throw new IllegalArgumentException(                    "Meal can't be order without any food item");        }        this.service = service;        this.foodItems = new ArrayList<FoodItem>(foodItems);        this.customerName = customerName;        if (this.customerName == null) {            this.customerName = "NO NAME";        }    }    public List<FoodItem> getFoodItems() {        return foodItems;    }    public String getCustomerName() {        return customerName;    }    public OrderService getService() {        return service;    }}

А вот код для OrderBuilder, который конструирует объект Order.

package org.trishinfotech.builder;import java.util.ArrayList;import java.util.List;import org.trishinfotech.builder.beverages.BeverageSize;import org.trishinfotech.builder.beverages.cold.CocaCola;import org.trishinfotech.builder.beverages.cold.Pepsi;import org.trishinfotech.builder.food.burger.ChickenBurger;import org.trishinfotech.builder.food.burger.VegBurger;import org.trishinfotech.builder.food.nuggets.CheeseNuggets;import org.trishinfotech.builder.food.nuggets.ChickenNuggets;import org.trishinfotech.builder.meal.FoodItem;import org.trishinfotech.builder.meal.Meal;public class OrderBuilder {    protected static final double HAPPY_MENU_DISCOUNT = 5.0d;    private String customerName;    private OrderService service = OrderService.TAKE_AWAY;    private List<FoodItem> items = new ArrayList<FoodItem>();    public OrderBuilder() {        super();    }      // Сеттеры для каждого поля в целевом объекте. В этом примере это Order.    // Возвращаемым типом у нас будет сам Builder (например, OrderBuilder), чтобы сделать возможным цепной вызов сеттеров.    public OrderBuilder name(String customerName) {        this.customerName = customerName;        return this;    }      public OrderBuilder service(OrderService service) {        if (service != null) {            this.service = service;        }        return this;    }    public OrderBuilder item(FoodItem item) {        items.add(item);        return this;    }    // Комбо предложения    public OrderBuilder vegNuggetsHappyMeal() {        List<FoodItem> foodItems = new ArrayList<FoodItem>();        foodItems.add(new CheeseNuggets());        foodItems.add(new Pepsi(BeverageSize.S));        Meal meal = new Meal("Veg Nuggets Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);        return item(meal);    }    public OrderBuilder chickenNuggetsHappyMeal() {        List<FoodItem> foodItems = new ArrayList<FoodItem>();        foodItems.add(new ChickenNuggets());        foodItems.add(new CocaCola(BeverageSize.S));        Meal meal = new Meal("Chicken Nuggets Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);        return item(meal);    }    public OrderBuilder vegBurgerHappyMeal() {        List<FoodItem> foodItems = new ArrayList<FoodItem>();        foodItems.add(new VegBurger());        foodItems.add(new Pepsi(BeverageSize.S));        Meal meal = new Meal("Veg Burger Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);        return item(meal);    }    public OrderBuilder chickenBurgerHappyMeal() {        List<FoodItem> foodItems = new ArrayList<FoodItem>();        foodItems.add(new ChickenBurger());        foodItems.add(new CocaCola(BeverageSize.S));        Meal meal = new Meal("Chicken Burger Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);        return item(meal);    }    public Order build() {        Order order = new Order(service, items, customerName);        if (!validateOrder()) {            System.out.println("Sorry! Order can't be placed without service type (Take Away/Eat Here) and any food item.");            return null;        }        return order;    }    private boolean validateOrder() {        return (service != null) && !items.isEmpty();    }}

Готово! Теперь пришло время написать Main для выполнения и тестирования результат:

package org.trishinfotech.builder;import org.trishinfotech.builder.beverages.BeverageSize;import org.trishinfotech.builder.beverages.cold.CocaCola;import org.trishinfotech.builder.beverages.cold.Pepsi;import org.trishinfotech.builder.beverages.hot.HotChocolate;import org.trishinfotech.builder.food.burger.ChickenBurger;import org.trishinfotech.builder.food.nuggets.CheeseNuggets;import org.trishinfotech.builder.food.nuggets.ChickenNuggets;import org.trishinfotech.builder.util.BillPrinter;public class Main {    public static void main(String[] args) {        OrderBuilder builder1 = new OrderBuilder();        // you can see the use of chained calls of setters here. No statement terminator        // till we set all the values of the object        Order meal1 = builder1.name("Brijesh").service(OrderService.TAKE_AWAY).item(new ChickenBurger())                .item(new Pepsi(BeverageSize.M)).vegNuggetsHappyMeal().build();        BillPrinter.printItemisedBill(meal1);        OrderBuilder builder2 = new OrderBuilder();        Order meal2 = builder2.name("Micheal").service(OrderService.EAT_HERE).item(new ChickenNuggets())                .item(new CheeseNuggets()).item(new CocaCola(BeverageSize.L)).chickenBurgerHappyMeal()                .item(new HotChocolate(BeverageSize.M)).vegBurgerHappyMeal().build();        BillPrinter.printItemisedBill(meal2);    }}

А вот и результат работы программы:

                                                  Food Court=================================================================================================================Service:  Take Away (2.00 Tax)                                                         Customer Name: Brijesh             -----------------------------------------------------------------------------------------------------------------                Food Item |   Calories |    Packing |      Price |   Packing Price | Discount % | Total Price-----------------------------------------------------------------------------------------------------------------           Chicken Burger |        300 |       Wrap |       4.50 |            0.40 |       0.00 |       4.90                Pepsi (M) |        220 |     Bottle |       2.20 |            0.75 |       0.00 |       2.95   Veg Nuggets Happy Meal |        490 | Multi-Pack |       5.00 |            2.00 |       5.00 |       6.75=================================================================================================================Date: 2020/10/09 20:02:38                                                     Total Bill (incl. taxes): 14.89Enjoy your meal!                                                  Food Court=================================================================================================================Service:   Eat Here (5.50 Tax)                                                         Customer Name: Micheal             -----------------------------------------------------------------------------------------------------------------                Food Item |   Calories |    Packing |      Price |   Packing Price | Discount % | Total Price-----------------------------------------------------------------------------------------------------------------          Chicken Nuggets |        450 |  Container |       5.00 |            1.25 |       0.00 |       6.25           Cheese Nuggets |        330 |  Container |       3.80 |            1.25 |       0.00 |       5.05            Coca-Cola (L) |        290 |     Bottle |       2.00 |            0.75 |       0.00 |       2.75Chicken Burger Happy Meal |        450 | Multi-Pack |       5.50 |            1.15 |       5.00 |       6.38        Hot Chocolate (M) |        450 | Sipper Mug |       2.30 |            1.60 |       0.00 |       3.90    Veg Burger Happy Meal |        340 | Multi-Pack |       3.90 |            1.15 |       5.00 |       4.86=================================================================================================================Date: 2020/10/09 20:02:38                                                     Total Bill (incl. taxes): 30.78Enjoy your meal!

Ну вот и все! Я надеюсь, что этот урок помог освоить паттерн Builder.

Исходный код можно найти здесь: Real-Builder-Design-Pattern-Source-Code

и здесь: Builder-Design-Pattern-Sample-Code


Узнать подробнее о курсе Архитектура и шаблоны проектирования.

Смотреть вебинар Шаблоны GRASP.

Подробнее..

Разделяй и властвуй Использование FSM в Unity

23.04.2021 22:09:05 | Автор: admin

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

Минимальный аниматор главного героя в платформереМинимальный аниматор главного героя в платформере

Аниматоры в Unity построены как раз на конечных автоматах. Каждая анимация группы объектов представлена в виде состояния. Условия и порядок переходов между ними определяется в аниматоре, который является конечным автоматом. Также, неоднократно поднималась тема использования конечных автоматов для описания логики работы объектов со сложным поведением. AI ботов, управление главным героем, вот это все.

Я бы хотел сделать акцент на менее изъезженной теме. Конечный автомат позволяет эффективно управлять состоянием игры в целом. Более того, с его помощью можно однажды описать бизнес-логику абстрактной игры, и отталкиваться в дальнейшем от этой заготовки при разработке прототипов. Так, многие ГК игры, построенные по такому принципу могут отличаться друг от друга на полсотни строк и структуру сцены, реализуя всю логику на автомате. А в сложнейших и запутанных проектах новый функционал может быть внедрен максимально просто путем добавления в существующий автомат нового состояния и добавления переходов в него, исключая возможность возникновения багов и неожиданного поведения. Также, среди плюсов построения бизнес-логики приложения на автоматах можно выделить следующее:

  • Каждое состояние описывает свою логику максимально просто. Нет никаких условных блоков, каждое состояние представляет собой последовательность действий на входе и выходе и список сигналов, которые автомат обрабатывает в текущем состоянии. Таким образом, мы отображаем джойстик на входе в состояние Play и скрываем на выходе. Но даже если каким-то чудом игрок увидел джойстик на экране паузы, он не сможет двигать персонажа. И не потому, что активен еще один флаг isPaused, а просто потому, что в состоянии паузы не предусмотрено принимать сигналы с джойстика.

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

  • В дополнение к предыдущему пункту, следует отметить, что это также очень удобно в контексте дебага. Посмотрев логи, мы сразу видим, что бот сломался потому, что был инициализирован не после старта уровня, а во время генерации сцены, когда еще не был готов контекст для работы его AI.

  • Многие баги становятся просто невозможны, потому что мы строго определяем условия переходов. Мы точно не попадем в состояние Play, пока состояние WaitMatch не получит сигнал "match_ready", а если мы захотим вернуться в лобби, мы сначала отправим серверу команду об этом, и только после сигнала "room_left" выполним переход.

  • Сама идея логики на автоматах максимально прозрачна. Видя ТЗ мы сразу понимаем, каким будет список состояний, логика, реализованная в каждом из них и граф переходов. Причем, как было отмечено выше, "внешняя" часть графа в большинстве игр будет оставаться неизменной.

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

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

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

FSM

AState

- public FSM(AState initState)

- public void Signal(string name, object data = null)

- private void ChangeState(AState newState)

- void Enter()

- void Exit()

- AState Signal()

Итак, мы имеем 2 сущности:

Конечный автомат. Интерфейс у него невелик. Начиная работу в некотором состоянии, он может принимать сигналы извне, передавая их в текущее состояние. И если оно в ответ на сигнал вернуло новое состояние, автомат выполняет переход в него. В момент перехода мы выполняем Exit для последнего состояния и Enter для следующего. Таким образом, у нас получается следующая конструкция:

 public class FSM {   private AState currentState;   public FSM(AState initState) => ChangeState(initState);      private void ChangeState(AState newState)   {     if (newState == null) return;     currentState?.Exit();     currentState = newState;     currentState.Enter();   }   public void Signal(string name, object arg = null)   {     var result = currentState.Signal(name, arg);     ChangeState(result);   } }

Абстрактное состояние. Из соображений удобства и микрооптимизации его методы имеют нехитрую реализацию, которая наследуется потомками, в которых оные не определены.

public class AState{  public virtual void Enter() => null;  public virtual void Exit() => null;  public virtual AState Signal(string name, object arg) => null;}

А в самих состояниях мы просто описываем логику

public class SLoad : AState{    public override void Enter()    {        Game.Data.Set("loader_visible",true);        var load = SceneManager.LoadSceneAsync("SceneGameplay");        load.completed+=a=>Game.Fsm.Signal("scene_loaded");    }    public override void Exit()    {        Game.Data.Set("loader_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "scene_loaded")            return new SLobby();        return null;    }    }

Как вы могли заметить, я описал логику переходов между состояниями, но даже вскользь не затронул тему хранения и изменения их списка. Дело в том, что в этом нет смысла, мы можем просто создавать новые состояния при переходе. Экземпляр состояния в большинстве случаев представляет собой 3 ссылки на функции, хранящиеся статически. И переходы между состояниями - не такое частое действие, чтобы дать сколько-то существенный оверхед. Зато мы получаем гору синтаксического сахара и статический анализ в подарок! При желании, проблема аллокаций решается очень легко, но это будет не так удобно. Кроме того, вот пример того, как такое состояние можно легко параметризовать, не усложняя при этом общий интерфейс

public class SMessage : AState{    private string msgText;    private AState next;    public SMessage(string messageText, AState nextState)    {        msgText = messageText;        btnText = buttonText;        next = nextState;    }        public override void Enter()    {        Game.Data.Set("message_text", msgText);        Game.Data.Set("window_message_visible",true);    }    public override void Exit()    {        Game.Data.Set("window_message_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "message_btn_ok")             return next;        return null;    }}

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

...case "iap_ok":return new SMessage("Item purchased! Going back to store.", new SStore());...

В этой статье я не буду задерживаться на устройстве структуры Game.Data, используемой в состояниях, достаточно просто отметить, что это словарь, в элементах которого реализован шаблон "Наблюдатель". Объекты сцены, например, элементы UI, могут подписываться на изменения в нем и отображать гарантированно актуальные данные. Она играет ключевую роль в связи между визуализацией и логикой в моих примерах, но не является необходимым условием для работы с конечными автоматами. Сигналы в автомат же можно отправлять откуда угодно, но одним из самых универсальных примеров будет следующий скрипт.

public class ButtonFSM : MonoBehaviour, IPointerClickHandler{    public string key;        public override void OnPointerClick(PointerEventData eventData)    {        Game.Fsm.Signal(key);    }}

Иными словами, мы при клике по кнопке(на самом деле, любому CanvasRenderer) передаем соответствующий сигнал в автомат. При переходе между состояниями мы можем любым удобным нам способом включать и выключать разные Canvas, менять маски, используемые в Physics.Raycast и даже иногда менять Time.timeScale! Как бы ужасно и бескультурно это ни казалось на первый взгляд, пока сделанное в Enter отменяется в Exit, оно гарантированно не может доставить каких-либо неудобств, так что вперед! Главное - не переусердствуйте.

Подробнее..

Маленькими шагами к красивым решениям

16.05.2021 12:20:18 | Автор: admin

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

Изученные правильные практики и шаблоны проектирования лишь вектор, который вдохновляет на красивые и уникальные решения.

Здесь нет примеров хорошей архитектуры, советов как должно быть и как правильно. Я просто хочу зафиксировать мысли, которые надо не забывать воспроизводить при решении очередной задачи.

Снаружи для пользователя, внутри для команды разработки

Архитектура системы определяет скорость ее развития. Красивая и правильная архитектура вдохновляет работать и помогает команде разработки.

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

Если система проста и удобна внутри, то она так же проста и удобна снаружи. У пользователей меньше проблем, а значит и у разработчиков тоже.

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

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

Модель

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

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

При реализации объектной модели клиентам рекомендуется ориентироваться на модель сервер-приложения.

Логика

Упрощайте алгоритмы. Делите сложные части на простые. Не переусердствуйте.

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

Если какая-то часть алгоритма многократно повторяется, то скорее всего она должна быть выделена в отдельный модуль и переиспользоваться. Отдельный модуль отдельный алгоритм.

Алгоритм должен легко читаться: в тексте, в любой нотации моделирования, в коде.

Не прячьте за методами в коде неявную логику. Неявные связи в реализации логики ведут к усложнению решения.

Когда отдельные части программы нельзя перекомбинировать в новое целое, значит создан неэффективный монолит.

Нейминг решает

Если назвать бокал стаканом, то назначение "пить" вроде бы не меняется, но что-то все-таки не то.

Имя объекта должно характеризовать его достаточно емко, не вызывать двусмысленности.

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

Имена объектов и методов в конечном счете становится языком команды разработки, заказчика ПО и даже пользователей.

MVP и прототипы

MVP это быстро и просто, это способ сдвинуться с мертвой точки и начать показывать результат.

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

Рефакторинг

Это придется делать. Это нормально и это важно. Даже для задач, выпущенных в продакшн совсем недавно.

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

Заботьтесь о том, чтобы в будущем код не вызывал страха и отвращения к рефакторингу. С ним должно быть приятно работать. Если нет четкой и понятной архитектуры, то программисты будут бояться даже заглянуть в код, чтобы понять как это работает.

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

Документация

Документация к системе должна дополнять программный код и хранить знания о причинах принятых архитектурных решений.

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

Выводы

Не усложнять.

Ничего не прятать.

Называть все своими именами.

Не стоять на месте.

Не поддаваться отчаянию, если не получилось.

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

Подробнее..

Перевод Что такое Service Mesh?

16.06.2020 16:14:27 | Автор: admin
И снова здравствуйте!.. В преддверии старта курса Архитектор ПО мы подготовили еще один полезный перевод.



Service Mesh это конфигурируемый инфраструктурный уровень с низкой задержкой, который нужен для обработки большого объема сетевых межпроцессных коммуникаций между программными интерфейсами приложения (API). Service Mesh обеспечивает быструю, надёжную и безопасную коммуникацию между контейнеризированными и часто эфемерными сервисами инфраструктуры приложений. Service Mesh предоставляет такие возможности, как обнаружение сервисов, балансировку нагрузки, шифрование, прозрачность, трассируемость, аутентификацию и авторизацию, а также поддержку шаблона автоматического выключения (circuit breaker).
Service Mesh обычно реализуется путем предоставления каждому экземпляру сервиса экземпляра прокси, который называется Sidecar. Sidecar обрабатывают коммуникации между сервисами, производят мониторинг и устраняют проблемы безопасности, то есть все, что может быть абстрагировано от отдельных сервисов. Таким образом, разработчики могут писать, поддерживать и обслуживать код приложения в сервисах, а системные администраторы могут работать с Service Mesh и запускать приложение.

Istio от Google, IBM и Lyft, на данный момент является самый известной Service Mesh-архитектурой. А Kubernetes, который изначально разрабатывался в Google, сейчас является единственным фреймворком для оркестрации контейнеров, который поддерживается Istio. Вендоры пытаются создать коммерческие поддерживаемые версии Istio. Интересно, что нового они смогут привнести в проект с открытым исходным кодом.

Однако Istio это не единственный вариант, поскольку разрабатываются и другие реализации Service Mesh. Паттерн sidecar proxy является наиболее популярной реализацией, как можно судить по проектам Buoyant, HashiCorp, Solo.io и другим. Существуют и альтернативные архитектуры: технологический инструментарий Netflix это один из подходов, где функционал Service Mesh реализуется за счет библиотек Ribbon, Hysterix, Eureka, Archaius, а также таких платформ, как Azure Service Fabric.

Service Mesh также имеет свою собственную терминологию для компонентов-сервисов и функций:

  • Фреймворк оркестрации контейнеров. По мере того, как все больше и больше контейнеров добавляется в инфраструктуру приложения, появляется необходимость в отдельном инструменте для мониторинга и управления контейнерами фреймворке оркестрации контейнеров. Kubernetes плотно занял эту нишу, причем настолько, что даже его основные конкуренты Docker Swarm и Mesosphere DC/OS предлагают в качестве альтернативы интеграцию с Kubernetes.
  • Сервисы и экземпляры (поды Kubernetes). Экземпляр это единственная запущенная копия микросервиса. Иногда один экземпляр это один контейнер. В Kubernetes экземпляр состоит из небольшой группы независимых контейнеров, который называется подом. Клиенты редко обращаются напрямую к экземпляру или поду, чаще они обращаются к сервису, который представляет из себя набор идентичных масштабируемых и отказоустойчивых экземпляров или подов (реплик).
  • Sidecar Proxy. Sidecar Proxy работает с одним экземпляром или подом. Смысл Sidecar Proxy в том, чтобы направлять или проксировать трафик, приходящий от контейнера, с которым он работает, и обратный трафик. Sidecar взаимодействует с другими Sidecar Proxy и управляется фреймворком оркестрации. Многие реализации Service Mesh используют Sidecar Proxy для перехвата и управления всем входящим и исходящим трафиком экземпляра или пода.
  • Обнаружение сервисов. Когда экземпляру нужно взаимодействовать с другим сервисом, ему нужно найти (обнаружить) исправный и доступный экземпляр другого сервиса. Как правило экземпляр выполняет поиск по DNS. Фреймворк оркестрации контейнеров хранит список экземпляров, которые готовы к получению запросов, и предоставляет интерфейс для DNS-запросов.
  • Балансировка нагрузки. Большинство фреймворков оркестрации контейнеров обеспечивают балансировку нагрузки на 4 уровне (транспортном). Service Mesh реализует более сложную балансировку нагрузки на 7 уровне (прикладном), богатую алгоритмами и более эффективную в вопросе управления трафиком. Параметры балансировки нагрузки могут быть изменены с помощью API, что позволяет оркестрировать сине-зеленое или канареечное развертывание.
  • Шифрование. Service Mesh может зашифровывать и расшифровывать запросы и ответы, снимая это бремя с сервисов. Service Mesh также может повысить производительность за счет приоритезации или переиспользования существующих постоянных соединений, что снижает необходимость в дорогих вычислениях для создания новых соединений. Наиболее распространенной реализацией шифрования трафика является mutual TLS (mTLS), где инфраструктура открытых ключей (PKI) генерирует и распространяет сертификаты и ключи для использования их в Sidecar Proxy.
  • Аутентификация и авторизация. Service Mesh может авторизовывать и аутентифицировать запросы сделанные снаружи или изнутри приложения, отправляя экземплярам только валидированные запросы.
  • Поддержка шаблона автоматического выключения. Service Mesh поддерживает шаблон автоматического выключения, который изолирует нездоровые экземпляры, а затем постепенно возвращает их в пул здоровых экземпляров при необходимости.

Та часть приложения Service Mesh, которая управляет сетевым трафиком между экземплярами называется Data Plane. Создание и развертывание конфигурации, которая управляет поведением Data Plane, выполняется с помощью отдельной Control Plane. Control Plane обычно включает в себя или спроектирована таким образом, чтобы подключаться к API, CLI или GUI для управления приложением.


Control Plane в Service Mesh распределяет конфигурацию между Sidecar Proxy и Data Plane.

Часто архитектура Service Mesh применяется для решения сложных операционных задач с использованием контейнеров и микросервисов. Пионерами в области микросервисов являются такие компании как Lyft, Netflix и Twitter, которые предоставляют стабильно работающие сервисы миллионам пользователей по всему миру. (Здесь вы можете познакомиться с подробным описанием некоторых архитектурных задач, с которыми столкнулся Netflix). Для менее требовательных прикладных задач скорее всего будет достаточно более простых архитектур.

Архитектура Service Mesh вряд ли когда-нибудь станет ответом на все вопросы, связанные с работой приложений и их доставкой. Архитекторы и разработчики обладают огромным арсеналом инструментов, и только один из них молоток, который среди множества задач должен решать лишь одну забивать гвозди. Microservices Reference Architecture от NGINX, например, включает в себя несколько различных моделей, которые дают непрерывный спектр подходов к решению задач с помощью микросервисов.

Элементы, которые объединяются в архитектуре Service Mesh, такие как NGINX, контейнеры, Kubernetes и микросервисы в качестве архитектурного подхода, могут быть не менее продуктивно использованы и в реализациях без Service Mesh. Например, Istio был разработан как полноценная архитектура Service Mesh, но модульность говорит о том, что разработчики могут выбирать и применять только необходимые им компоненты технологий. Помня об этом, необходимо сформировать четкое понимание концепции Service Mesh, даже если вы не уверены в том, что сможете когда-нибудь полноценно ее реализовать в приложении.



Модульные монолиты и DDD


Подробнее..

Перевод Архитектура микросервисов Разрушение монолита

01.06.2021 20:15:10 | Автор: admin

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

Эта статья подводит итог вебинара "Разрушение монолита", представленного Даниэлем Гутьерресом Сааведрой, старшим инженером-программистом компании Zartis. Вы можете посмотреть полный текст вебинара, который также включает сессию вопросов и ответов, ниже!

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

Почему стоит выбрать микросервисы

Микросервисы разрабатываются с использованием бизнес-ориентированных API для инкапсуляции основных бизнес-возможностей. Принцип слабой связи (loose coupling) помогает устранить или минимизировать зависимости между сервисами и их потребителями.

Кроме всего прочего, они являются:

  • Масштабируемыми

  • Управляемыми

  • Поставляемыми

  • Гибкими

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

Проблемы микросервисов

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

Вот некоторые из основных проблем, которые необходимо учитывать:

  • Дополнительные уровни сложности.

  • Если ваше программное обеспечение не меняется часто, оно может ничего не исправить.

  • Приобретение новых продуктов требует дополнительных затрат.

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

Разрушение монолита

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

Шаг 1: Определение основных услуг

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

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

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

Шаг 2: Разделение и рефакторинг

Итак, мы увидели, как можно все разделить, но как теперь отделить сервисы от всего остального и рефакторить их, чтобы они стали набором микросервисов?

Первое, что нужно знать: чем меньше коммуникаций, тем лучше отношения. Очень легко и заманчиво создать множество сервисов, которые удобно тестировать по отдельности, но в целом ваша система станет сложной и запутанной. Это затрудняет отслеживание в случае появления проблемы, поскольку возникает огромная путаница, и может быть трудно определить, где кроется корень проблемы.

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

Шаг 3: API и облако

Теперь, когда мы сделали все нарезки и разделили наш код, куда мы можем поместить все это? В облако!

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

Если назвать несколько наиболее распространенных, то Google Cloud (GCP), Microsoft Azure и AWS это три основных претендента, но есть и много других поставщиков. Эти решения обычно предоставляют готовую архитектуру микросервисов, где вам нужно только сделать несколько штрихов и провести небольшое обучение, чтобы все заработало.

Кроме того, есть решения, которые необходимы на месте, потому что вы не хотите размещать свои сервисы в облаке. Например, применив сервисы Spring Cloud, можно использовать свои собственные серверы и идеально имитировать структуры публичного облака.

Сколько может стоить миграция на микросервисы с использованием облачных решений?

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

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

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

Распространенные стратегии миграции

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

Шаблон Strangler

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

В приведенном ниже случае было решено извлечь всю клиентскую часть приложения в архитектуру микросервисов и оставить административную часть в монолите, что вполне нормально. Здесь значительно расширили свой код и смогли сделать это, не останавливая разработку. Но это не должно быть окончательным состоянием приложения.В идеале все должно оказаться в правой части изображения. Как вы можете видеть, там есть DBF, то есть DB (база данных) для каждой службы. Это не обязательное требование, но оно помогает.

Параллельная разработка

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

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

Заключение

Когда мы находимся в процессе миграции, важно не забегать вперед и пытаться вносить изменения параллельно или проектировать их заранее.

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


Перевод статьи подготовлен в рамках курса "Microservice Architecture". Если вам интересно узнать о курсе подробнее, приходите на день открытых дверей онлайн, где преподаватель расскажет о формате и программе обучения.

Подробнее..

Архитектура кода программного обеспечения декорируем стратегией. Рассказ в 10 эпизодах, основанный на реальных событиях

21.05.2021 08:10:50 | Автор: admin

Встречаются два эксперта-консультанта по конструированию программного обеспечения:
- Как написать сложное корпоративное приложение, поддерживать которое будет всегда легко и дешево.
- Могу рассказать...
- Рассказать и я могу! Написать-то как?..

Время чтения: 25 мин.

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

Есть много замечательной доступной литературы с теорией. Найти теорию не проблема; проблема применить найденную теорию на практике. Я являюсь сторонником конструирования исключительно поддерживаемого кода, всегда стараюсь найти новые способствующие этому подходы. К сожалению, часто подобные поиски тщетны. Приходится набираться опыта разработки поддерживаемых приложений самостоятельно, придумывать различные подходы. В этой статье хочу поделиться практическими знаниями о проектировании архитектуры кода программного обеспечения, полученными из опыта.

В самом начале статьи хотел бы заранее попросить прощения у читателя за "много букв". Честно говоря, пробовал выразить свою мысль в более короткой версии статьи всё время казалось, что не хватает важных деталей... Надеюсь, статья будет вам интересна и полезна.

Введение в предметную область

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

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

В этой статье я хочу предложить технику написания программ, в основе которой лежит два паттерна проектирования ООП: декоратор и стратегия. Я уверен, что основная часть читающих статью наверняка не раз сталкивалась с этими паттернами (возможно, даже на практике). Но чтобы все чувствовали себя "в своей тарелке", обращусь к определениям из "Паттернов проектирования" Эриха Гаммы, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса (Банда четырех, Gang of Four, GoF):

  • Декоратор (Decorator, Wrapper) паттерн проектирования, позволяющий динамически добавлять объекту новые обязанности. Является гибкой альтернативой порождению подклассов с целью расширения функциональности.

  • Стратегия (Strategy, Policy) паттерн проектирования, который определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются.

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

Декорирование стратегией, на мой взгляд, даёт великую пользу при поддержке приложений на очень большом жизненном цикле программного продукта. Компоненты в коде, написанные с применением данного подхода, соответствуют всем принципам дизайна SOLID из "Чистой архитектуры" Роберта Мартина. Каждый компонент, который мы напишем далее, будет отвечать только за одно действие; после написания нового компонента мы ни разу не модифицируем логику его методов, а лишь будем расширять ее в декорирующих компонентах; в силу паттерна "Декоратор" все расширяемые и расширяющие компоненты соответствуют одному контракту, следовательно их можно заменять друг другом; интерфейсы компонентов не содержат зависимостей, которые не используются; компоненты бизнес-логики ни в коей мере не зависят от деталей.

Я не раз сталкивался в обсуждениях с опытными разработчиками, которые говорят: "А вот всё, что связано с применением принципов SOLID, паттернов ООП на практике это миф!". Любезно обращаясь к скептически настроенным к применению теории разработки в реальных больших корпоративных проектах, хочу сказать: "А вот посмотрим!"

Предлагаю обозначить несколько условностей. Код приводить я буду на языке Golang. Конечно Go не самый лучший язык для демонстрации "фишек" ООП, но, во-первых, так мы покажем, что применение паттернов проектирования не должно страдать от выбора языка программирования, ибо язык это априори инструмент, а во-вторых, для меня данный язык на сей день ближе всего находится к нашим реальным корпоративным проектам, которые успешно работают в продакшне.

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

  • Должная обработка ошибок. В коде мы ограничимся оборачиванием ошибок дополнительным сообщением с помощью пакета "github.com/pkg/errors".

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

  • Комментарии и документирование кода.

  • Всё, что связано, с конкурентным выполнением задач и синхронизацией.

  • Структура файлов и директорий проекта.

  • Стили, линтеры и статический анализ.

  • Покрытие кода тестами.

  • Сквозь методы компонентов рекомендуется с первых этапов разработки "тянуть" context.Context, даже если он в тот момент не будет использоваться. Для упрощения повествования в примерах далее контекст также использоваться не будет.

Перейдём же наконец от скучной теории к занимательной практике!

Пролог. Закладываем фундамент

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

Итак, начнём. Здесь мы с вами высококвалифицированные разработчики программных продуктов. К нам приходит наш первый заказчик от бизнеса и говорит что-то вроде: "Нам нужна функциональность обновления такой-то информации о пользователях нашей платформы". Мы обрабатываем требования, продумываем архитектуру и переходим к конструированию кода.

Первое, что нужно сделать определить интерфейс нашего первого компонента службы, которая будет представлять желаемый use-case SavePersonService. Но для этого нам нужно определить объекты нашей предметной области, а именно структуру данных, содержащую информацию о человеке PersonDetails. Создадим в корне проекта пакет app, далее создадим файл app/person.go, и оставим в нём нашу структуру:

// app/person.gotype PersonDetails struct {    Name string    Age  int}

Данный файл завершён, больше мы к нему в этой статье возвращаться не будем. Далее создаем файл app/save-person.go, и определяем в нём интерфейс нашего use-case:

// app/save-person.gotype SavePersonService interface {    SavePerson(id int, details PersonDetails) error}

Оставим сразу рядом с определением интерфейса его первую реализацию компонент noSavePersonService, который ничего не делает в теле интерфейсного метода:

// app/save-person.go// ... предыдущий код ...type noSavePersonService struct{}func (noSavePersonService) SavePerson(_ int, _ PersonDetails) error { return nil }

Поскольку объекты noSavePersonService не содержат состояния, можно гарантировать, что данный "класс" может иметь только один экземпляр. Напоминает паттерн проектирования Синглтон (Singleton ещё его называют Одиночка, но мне это название по ряду причин не нравится). Предоставим глобальную точку доступа к нему. В Golang легче всего это сделать, определив глобальную переменную:

/ app/save-person.go// ... предыдущий код ...var NoSavePersonService = noSavePersonService{}

Зачем мы написали ничего не делающий компонент? С первого взгляда он очень походит на заглушку. Это не совсем так. Далее поймём.

Эпизод 1. Будем знакомы, Декоратор Стратегией

Перейдём непосредственно к реализации бизнес-логики нашей задачи. Нам нужно в конечном счёте иметь хранилище, в котором содержатся данные о пользователях. С точки зрения выбора технологии мы сразу себе представляем, что будем использовать PostgreSQL, но правильно ли завязываться в коде нашей бизнес-логики на конкретную технологию. Вы правы конечно нет. Определить компонент нашего хранилища нам позволит паттерн Репозиторий (Repository). Создадим пакет с реализациями интерфейса нашего use-case save-person внутри app, и в нём создадим файл app/save-person/saving_into_repository.go реализации нашего use-case, которая обновляет данные в репозитории:

// app/save-person/saving_into_repository.gotype PersonRepository interface {    UpdatePerson(id int, details app.PersonDetails) error}type SavePersonIntoRepositoryService struct {    base app.SavePersonService    repo PersonRepository}func WithSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}func (s SavePersonIntoRepositoryService) SavePerson(id int, details app.PersonDetails) error {    err := s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in save person into repository service")    }    err = s.repo.UpdatePerson(id, details)    if err != nil {        return errors.Wrap(err, "update person in repo")    }    return nil}

В коде выше впервые появляется компонент, который выражает наш подход "Декорирование стратегией". Сам компонент представляет собой декоратор, реализующий интерфейс нашего use-case, который оборачивает любой компонент с таким же интерфейсом. В реализации метода изначально вызывается метод декорируемого объекта s.base; после этого происходит вызов стратегии обновления данных о человеке в хранилище s.repo. По сути, весь подход это конструирование компонентов-декораторов, которые содержат два объекта:

  1. Непосредственно декорируемый объект с таким же интерфейсом.

  2. Стратегия, логику которой мы добавляем в довесок к логике декорируемого объекта.

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

Компонент сам по себе настолько прост, что самое сложное, пожалуй, это определить, когда следует вызывать метод стратегии до или после вызова метода декорируемого объекта или конкурентно с ним.

Напомню, что бизнес-логика не должна содержать ненужные зависимости, зависимости от деталей и т.п. Другими словами, бизнес-логика должна быть "чистая, как слеза". Где тогда должны находиться зависимости от конкретных реализаций, зависимости от используемых технологий? Ответ в файле main.go. Следуя замечаниям Роберта Мартина, можно сделать умозаключение, что код компонентов файла, содержащего точку входа в программу, является самым "грязным" с точки зрения зависимостей от всего. Обозначим в main.go метод, который нам возвращает клиент к базе данных PostgreSQL. И собственно сборку объекта службы нашего use-case и вызов его метода на условных входных данных:

// main.gofunc NewPostgreSQLDatabaseClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}func run() error {    userService := savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable"))    err := userService.SavePerson(5, app.PersonDetails{        Name: "Mary",        Age:  17,    })    if err != nil {        return errors.Wrap(err, "save user Mary")    }    return nil}

В коде выше мы можем заметить, что в качестве стратегии репозитория выступает обозначенный конкретный компонент клиента к PostgreSQL. В качестве же декорируемого объекта выступает наша "фиктивная" реализация use-case app.NoSavePersonService, которая по сути ничего не делает. Зачем она нужна? Она ничего полезного ведь не делает? Не легче ли просто вызвать метод клиента к базе данных? Спокойно, звёздный час этой реализации сейчас настанет.

Ссылка на полный код эпизода

Эпизод 2. Магия начинается!

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

// main.go// ... предыдущий код ...func NewMemoryCache() savePerson.PersonRepository {    // TODO implement    panic("not implemented")}// ... последующий код ...

Так как этот компонент реализует интерфейс нашего репозитория, мы можем очень изящно выполнить поставленную задачу, не меняя кода бизнес-логики, а всего лишь дополнительно обернуть наш компонент службы в main.go, создав новый, который использует также стратегию сохранения пользователя в кэш:

// main.go// внутри run()userService := savePerson.WithSavingPersonIntoRepository(    savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable")),    NewMemoryCache(),)err := userService.SavePerson(5, app.PersonDetails{    Name: "Mary",    Age:  17,})if err != nil {    return errors.Wrap(err, "save user Mary")}

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 3. Рефакторинг для здоровья

В предыдущем листинге кода создание сервиса выглядит достаточно громоздко. Нетрудно догадаться, применяя наш подход, мы продолжим и далее всё больше и больше оборачивать компонент, добавляя к логике новые стратегии. Поэтому мы, как опытные разработчики, замечаем эту потенциальную трудность и производим небольшой рефакторинг когда. Нам поможет паттерн Билдер (Builder опять же мне не очень нравится ещё одно его название Строитель). Это будет отдельный компонент, зона ответственности которого предоставить возможность сборки объекта службы нашего use-case. Файл app/save-person/builder.go:

// app/save-person/builder.gotype Builder struct {    service app.SavePersonService}func BuildIdleService() *Builder {    return &Builder{        service: app.NoSavePersonService,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    return b.service.SavePerson(id, details)}

Компонент Builder должен обязательно реализовывать интерфейс службы нашего use-case, так как именно он будет использоваться в конечном счёте. Поэтому мы добавляем метод SavePerson, который вызывает одноименный метод объекта в приватном поле service. Конструктор данного компонента называется BuildIdleService, потому что создаёт объект, который ничего не будет делать при вызове SavePerson (нетрудно заметить инициализацию поля service объектом app.NoSavePersonService). Зачем нам нужен этот бесполезный компонент? Чтобы получить всю истинную пользу, необходимо обогатить его другими методами. Эти методы будут принимать в параметрах стратегию и декорировать ею объект службы в поле service. Но вначале сделаем конструктор WithSavingPersonIntoRepository в app/save-person/saving_into_repository.go приватным, так как для создания службы мы теперь будем использовать только Builder:

// app/save-person/saving_into_repository.go// ... предыдущий код ...func withSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}// ... последующий код ...

Добавляем соответствующий метод для Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithSavingPersonIntoRepository(repo PersonRepository) *Builder {    b.service = withSavingPersonIntoRepository(b.service, repo)    return b}

И наконец производим рефакторинг в main.go:

// main.go// ... предыдущий код ...userService := savePerson.BuildIdleService().        WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).        WithSavingPersonIntoRepository(NewMemoryCache())// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 4. Больше заказчиков!

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

// main.go// ... предыдущий код ...func NewMongoDBClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}// ... последующий код ...

Воспользуемся нашим билдером и просто добавим новый код в main.go под имеющийся фрагмент с userService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache())err = taxpayerService.SavePerson(1326423, app.PersonDetails{    Name: "Jack",    Age:  37,})if err != nil {    return errors.Wrap(err, "save taxpayer Jack")}

Мы выполнили уже столько поставленных задач, имея небольшой фрагмент кода бизнес-логики. Заметьте, изменения преимущественно вносятся в файл main.go

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 5. Путь в никуда

Проходит ещё время. Заказчик 2 ставит нам такую задачу. Так как все налогоплательщики должны быть совершеннолетними, необходимо в бизнес-логику добавить функциональность проверки возраста человека перед сохранением в хранилище. С этого момента начинаются интересные вещи. Мы можем добавить эту валидацию в метод SavePersonIntoRepositoryService.SavePerson в файле app/save-person/saving_into_repository.go. Но тогда при нескольких декорированиях стратегией сохранения информации в репозиторий эта валидация будет вызываться столько раз, сколько производилось таких декораций. Хотя и все проверки помимо первой никак не влияют на результат напрямую, всё-таки не хочется лишний раз вызывать один и тот же метод.

Мы можем добавить валидацию в Builder.SavePerson. Но есть проблема: заказчику 1 не нужна проверка возраста при сохранении. Придётся добавить if и дополнительный флаг в параметры конструктора, который будет определять необходимость валидации:

// app/save-person/builder.gotype Builder struct {    service           app.SavePersonService    withAgeValidation bool}func BuildIdleService(withAgeValidation bool) *Builder {    return &Builder{        service:           app.NoSavePersonService,        withAgeValidation: withAgeValidation,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    if b.withAgeValidation && details.Age < 18 {        return errors.New("invalid age")    }    return b.service.SavePerson(id, details)}// ... последующий код ...

И тогда в main.go нужно вызывать конструкторы билдера с разными значениями флага withAgeValidation:

// main.go// ... предыдущий код ... userService := savePerson.BuildIdleService(false).// ... код ...taxpayerService := savePerson.BuildIdleService(true).// ... последующий код ...

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 6. Путь истины

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

Добавим новый компонент, который будет отвечать за валидацию при сохранении информации о людях:

// app/save-person/validating.gotype PersonValidator interface {    ValidatePerson(details app.PersonDetails) error}type PreValidatePersonService struct {    base      app.SavePersonService    validator PersonValidator}func withPreValidatingPerson(base app.SavePersonService, validator PersonValidator) PreValidatePersonService {    return PreValidatePersonService{base: base, validator: validator}}func (s PreValidatePersonService) SavePerson(id int, details app.PersonDetails) error {    err := s.validator.ValidatePerson(details)    if err != nil {        return errors.Wrap(err, "validate person")    }    err = s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in pre validate person service")    }    return nil}

Опять ничего нового. PreValidatePersonService это очередной декоратор стратегией валидации перед последующим вызовом декорируемого метода.

Добавим соответствующий метод в Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithPreValidatingPerson(validator PersonValidator) *Builder {    b.service = withPreValidatingPerson(b.service, validator)    return b}

Добавление каждого нового декоратора стратегией требует добавление нового метода в наш билдер.

Добавим реализацию валидатора, проверяющую возраст человека:

// main.go// ... предыдущий код ...type personAgeValidator struct{}func (personAgeValidator) ValidatePerson(details app.PersonDetails) error {    if details.Age < 18 {        return errors.New("invalid age")    }    return nil}var PersonAgeValidator = personAgeValidator{}// ... последующий код ...

Так как personAgeValidator не имеет состояния, можем сделать для компонента единую точку доступа PersonAgeValidator. Далее просто вызываем новый метод в main.go только для taxpayerService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 7. А ну-ка закрепим

Уверен, к данному эпизоду вы поняли смысл подхода "Декорирование стратегией". Чтобы закрепить, давайте добавим ещё один такой компонент. Представим, технический руководитель требует от нас покрыть метриками время выполнения сохранения данных в хранилище. Мы могли бы замерить это время, просто добавив пару строчек кода в SavePersonIntoRepositoryService. Но как бы не так! Мы же не изменяем уже работающий в продакшне код, а можем его только расширить. Давайте же так и сделаем. Добавим новый декоратор стратегией отправки метрики времени:

// app/save-person/sending_metric.gotype MetricSender interface {    SendDurationMetric(metricName string, d time.Duration)}type SendMetricService struct {    base         app.SavePersonService    metricSender MetricSender    metricName   string}func withMetricSending(base app.SavePersonService, metricSender MetricSender, metricName string) SendMetricService {    return SendMetricService{base: base, metricSender: metricSender, metricName: metricName}}func (s SendMetricService) SavePerson(id int, details app.PersonDetails) error {    startTime := time.Now()    err := s.base.SavePerson(id, details)    s.metricSender.SendDurationMetric(s.metricName, time.Since(startTime))    if err != nil {        return errors.Wrap(err, "save person in base in sending metric service")    }    return nil}

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

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithMetricSending(metricSender MetricSender, metricName string) *Builder {    b.service = withMetricSending(b.service, metricSender, metricName)    return b}

И наконец обозначаем в main.go функцию, возвращающую savePerson.MetricSender и добавляем вызов нового метода Builder в сборку наших сервисов:

// main.go// ... предыдущий код ...func MetricSender() savePerson.MetricSender {    // TODO implement    panic("not implemented")}// ... код ...userService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).    WithMetricSending(MetricSender(), "save-into-postgresql-duration").    WithSavingPersonIntoRepository(NewMemoryCache())// ... код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

Обратите внимание, что новые методы мы ставим в цепочку вызовов там, где мы хотим производить замер.

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 8. Результаты ясновидения

Проходит время. Заказчик 2 ставит новую задачу. Он желает знать, как долго выполняется сохранение данных о налогоплательщике, но с небольшой оговоркой: учитывать нужно всё, кроме валидации. Похоже на замер времени, который мы недавно реализовали для своих целей, не правда ли? Чтобы решить задачу, всё что нам требуется это добавить вызов метода для новой метрики в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithMetricSending(MetricSender(), "save-taxpayer-duration").    WithPreValidatingPerson(PersonAgeValidator)

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 9. Укрощение капризов

Мы вот только недавно произвели релиз последней задачи от заказчика 2, но он захотел изменить начальные требования. Такие изменения часто возникают на стороне заказчика, которые заставляют нас "перелопатить" весь код. Знакомо? На этот раз заказчик желает отказаться от оговорки из предыдущего эпизода и производить замер полного цикла сохранения данных о налогоплательщике вместе с валидацией. Если бы мы конструировали нашу бизнес-логику в виде сценария транзакции (transaction script), то это повлекло бы за собой непосредственное вмешательство в тело метода, copy-paste кода, что требует приложить силы, в том числе в процессе ревью, тестирования и т.п. В нашем же случае нам достаточно просто подвинуть вызов метода WithMetricSending в цепочке методов создания объекта службы в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator).    WithMetricSending(MetricSender(), "save-taxpayer-duration")

В коде выше мы поменяли местами второй WithMetricSending и WithPreValidatingPerson.

Задача от заказчика выглядит надуманной. Но напомню, что цель статьи не придумать качественные задачи заказчиков, а продемонстрировать пользу архитектуры кода при использовании подхода "Декорирование стратегией".

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 10. Взгляд в будущее

Этот заключительный эпизод всего лишь подчеркивает потенциал дальнейших доработок логики данного кода. Что ещё может пожелать заказчик от бизнеса или с технической стороны? Вариантов более чем достаточно. Может потребоваться функциональность отправки асинхронных событий об изменении информации о человеке (полезно при ведении журнала аудита, коммуникации с другими сервисами и т.д.). Может понадобиться введение механизма гомогенных и даже гетерогенных транзакций. Возможно, потребуется добавить запрос данных к соседнему микросервису. По техническим соображениям возможно будет нужен предохранитель (circuit-breaker) для таких запросов к другим сервисам. Наверняка нужно будет добавлять механизм трассировки (tracing). И многое-многое другое.

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

Эпилог. Подводим итоги

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

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

Но для больших корпоративных приложений наличие подобного подхода просто желательно-обязательно. Если продукт подразумевает длительную поддержку (обычно это условие присутствует всегда), то объектная модель приложения будет иметь значительное преимущество над незамысловатым "полотном" кода сценария транзакции. Я приведу далее график, в основе которого лежит график из "Шаблонов корпоративных приложений" Мартина Фаулера.

Что есть что на этом графике? Почему на осях нет чисел? Всё потому что график абстрактный. Он отражает качественный смысл содержимого, не количественный. По горизонтальной оси у нас время, прошедшее с момента начала разработки продукта. Или если желаете, количество добавлений новой функциональности в изначально разработанный продукт. Меру по вертикальной оси тоже можно выразить различными способами. Это может быть цена добавления новой строчки кода функционала в денежном эквиваленте; может быть время добавления новой функциональности; может быть количество потраченных нервных клеток разработчиком, ревьювером или тестировщиком. Красный график демонстрирует зависимость этих величин для подхода разработки, который называется сценарием транзакции (Transaction Script) последовательно следующие друг за другом инструкции. Синий график показывает эту зависимость для подхода модели предметной области (Domain Model).

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

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

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

Литература

  1. Макконнелл С. Совершенный код. Мастер-класс., 2020.

  2. Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования., 2020.

  3. Мартин Р. Чистая архитектура. Искусство разработки программного обеспечения., 2020

  4. Фаулер, Мартин. Шаблоны корпоративных приложений., 2020.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru