Авокадо с зубами подсказывает, что так код легче поддерживать, дописывать и рефакторить. Мы всё теперь пишем только так.
Привет, Хабр! У нас была проблема: каждый писал код как хотел. Было очень тяжело это поддерживать и ревьюить. Мы сначала думали, что достаточно написать стандарт кода. Оказалось, недостаточно, ему ещё надо обучить. Чтобы обучить, мы открыли для ревью эталоны кода, чтобы покрыть ими самую частую логику взаимодействия с компонентами. Тоже не хватило. А заодно я узнал, что мои же «золотые» образцы противоречили моему же стандарту кода (сначала было смешно, а потом пришлось переписывать).
В итоге я сделал кукбук с большим количеством примеров, чтобы объяснить культуру и методологию не через абстракции, а очень предметно. Начал вроде как просто для себя, оказалось полезно — и внедрил в работу команды.
Всё это было нужно для того, чтобы мы все в команде понимали, что такое наш стандарт кода, делали одинаковые вещи примерно одинаково, имели набор готовых шаблонов на системные или платформенные обращения и уменьшили объёмы как самого ревью, так и диалогов после.
Началось с того, что однажды я увидел очередной большой кусок кода, где логика из одного сервиса ушла в другой, а интеграционных тестов было аж 54 штуки. Исправленная опечатка в одном из запросов потребовала трёх часов работы, хотя я заранее знал с точностью до строчки и знака, где именно эта опечатка.
В общем, тот результат, который нас устроил, — это сочетание стандарта, библиотеки шаблонов и книги, как написать приложение. В ней мы делаем демоприложение и через все этапы показываем, почему принималось то или иное решение для реализации кода.
На сегодня ни в одной компании в России я не видел целостного подхода в сборке с кукбуком. Какие-то части встречаются, в сборке — нет. Вот я и хочу рассказать, как мы это сделали у себя в банке.
Посвящается всем, кто коллекционирует элегантные решения без привязки к языку, фрэймворку, Фаундлингам и Software Craftsmanʼам.
Погнали.
Самое главное
Вот ссылка на опубликованный CookBook: https://git.codemonsters.team/guides/ddd-code-toolkit/src/branch/dev
Почему не хватило стандартов разработки?
Потому что в моих исходных кодах по стандартам содержались предпосылки к ошибкам. Вот пример:
Сервис с интеграциями и бизнес-логикой
Ещё одна причина, почему стандарты полностью меня не удовлетворили, — необходим постоянный контроль качества кода по стандартам, евангелистская деятельность. Как показала практика, хорошие стандарты с рекомендациями от Роберта Мартина без надлежащего контроля не создают строгого коридора, из которого разработчики не выскочат в реактивную лапшу по пайплайну деливери, не раскидав по углам бизнес-логику.
Например, мы используем Domain Driven Design приложений, что очень хорошо защищает от расползания логики по тем местам, где её в идеале быть не должно.
Cила этой истории в том, что она собрала много полезных паттернов вместе, и это очень хорошо работает.
Впрочем, давайте прямо с самого начала.
Кукбук: зачем он нужен
Кукбук создан для погружения в разработку без перегрузок на абстракциях, его разрабы сразу начинают применять. Пришёл человек в команду, и в первую неделю — погружение. На второй неделе уже используют его на практике по тикету в джирке.
Кукбук содержит в себе паттерны применения стратегических и тактических паттернов DDD, Type Driven Development, Reach Domain Model, запущенных в функциональной парадигме с прагматичными юнит-тестами бизнес-логики без исключений по паттерну R.O.P (Railway Oriented Programming).
Всё это помогает:
- Превратить код в документацию.
- Обеспечить упрощённую архитектуру приложения в отличие от стандартной трёхзвенной компоновки пакетов.
- Решать базовые паттерны продуктовых команд финтека и не только.
Временами после работы я рублюсь на Flutter + kotlin backend и радуюсь, что подходы из кукбука — как неокиберпоэзия: работают на всех уровнях — от тупого фронта до хитроумного мидла с бэком.
Вся эта история проверена на практике промом и совершенно разными командами и задачами.
Как он появился
За 2,5 года в роли тимлида и техлида разработки, отвечающего как за качество delivery, так и за качество кода, я с командами затащил один проект в прод, и сейчас катим, как танки, второй.
В роли функционального играющего тренера мне пришлось столкнуться с рядом интересных вызовов: с одной стороны — много встреч, и нужно разные вопросы решать — от безопасности до солюшена, с другой — делегировать разработку и не просесть в качестве кода в реализуемых сервисах. Всё как у всех.
Делегируя разработку с достойными наработками по трёхзвенной архитектуре со слабой доменной моделью и классами-сервисами, в которых зашита логика, интеграционными тестами на логику, внешними огуречными тестами, я увидел, как разработка начинает огребать в функциональной реактивной парадигме по гайдам от Роберта Мартина с понятными именами, методами и чрезмерно гранулированными классами по SOLID.
Чуть не углядишь — намечается грязь, логика начинает расползаться по сервисам и прочее дерьмо. Меня это не устраивало: хочу, чтобы были паттерны, по которым можно направить и писать в сторонке спокойно по тикету задачу, а они пусть сами блюдут красоту и растут.
Обрисую постфактум мою задачу:
Без алкоголя сложно порой высвободить внутреннего Паланика, Хэмингуэя или Аллена. Погнали дальше, Максим.
Благодаря крутым работам инженеров Скота Влашина, Владимира Хорикова, Роберта Мартина, мадам Рефлексии и поискам удалось собрать воедино кукбук по реализации сервиса с бизнес-логикой (агрегирующие сервисы, перекладчики и прочие вспомогательные истории веб-разработки он также покрывает).
Новичку он призван показать, насколько важны методологии DDD, TDD (прагматичные тесты), как прекрасен Type Driven Development, как необходима грамотная коммуникация, что в функциональной парадигме можно понятно и просто писать. Можно и без неё. И что суперважно быть экспертом в той области, которую моделируешь.
Чтобы не тратить время на абстракции типа «чистый код» и «понятный код», кукбук содержит исходники, которые описывают шаблонную задачу веб-разработки: получить на вход данные, агрегировать их, принять решение по бизнес-логике и инициировать дальнейшие шаги.
Это достаточно жёсткое кунг-фу, пропитанное олдскульной классической школой тестирования, сильной доменной моделью и функциональной парадигмой.
Просто делайте по кукбуку тикет — будет хорошо, и начинайте изучать подробнее кирпичики, без которых его не выстроить. Баста!
Когда возникают проблемы, можно было описать пару template-сервисов и сказать: «Делайте так!» — но чтобы грамотно определить место бизнес-логике, без DDD не обойтись.
Интуиция, грамотные решения, руководитель разработки, долгие размусоленные встречи про то, какой сервис что делает и где какая бизнес-логика, — это всё круто, и за это (в том числе) нам банки платят. А ещё можно две недели одну кнопку в спринт затаскивать — всякое бывает.
Но я любитель проектов двух типов: мандатных и супервыгодных компаний. В таких условиях драйв другой, вызовы, темп, премии.
Нужна история более строгая и масштабируемая.
Кукбук — как раз об этом: как превратить реактивный код в документацию и юнит-тестами покрыть бизнес-логику.
Если вы ищете элегантные решения — он точно вас зацепит, и, хочется верить, вы унесёте с собой его часть или целиком.
Если вас не впирает мой стиль повествования — кукбук сразу по ссылке в конце поста, прокручивайте вниз, берите и пользуйтесь.
Для въедливых я немного пройдусь по каждому из разделов.
DDD — самое главное
Мы начали с того, что разработчик становится экспертом в той области, которую он описывает, и много внимания уделяем общему языку. Применяем паттерн Ubiquity Language.
Качественная разработка — это результат качественной коммуникации, а не программирование по постановке.
Я видел истории про страдающих и за всё отвечающих аналитиков, такие истории до добра не доводят. Я знаю, что есть команды, где аналитики всё за всех решают и говорят, как всё должно работать. Ещё и артефакты катят в пром. А разработчики сидят за удобной оградкой — ждут спецификации, и чуть что: «У меня лапки, мне так аналитики сказали».
В стагнирующем проекте такой подход работает и помогает коротать срок в период выплат ипотеки. У нас не так в командах.
Пока разработчик не поймёт, что нужно разработать, — ничего путного не разработает.
Что утаивали и Мартин, и Кент, который Бекк, — что разработчик должен стать солюшеном (ссылки на их труды — тоже внизу поста). Они такие крутые книги написали, потому как сами во всём разобрались, были архитекторами и порционно выдавали рекомендацию. А дальше зовите нас — мы проконсультируем.
Так вот, мы качаем разрабов — они рулят разработкой, аналитиков меньше в командах, и они помогают — разгружают разрабов. Отвечают за качество разработки и delivery у нас разрабы, а также активно тестируют, но это другая история.
Твой код — твоя зона ответственности. Мы обсуждаем задачу и просто описываем, что нужно сделать. Не разбираемся с талмудами аналитики, которая приходит как постановка, а просто задаёмся вопросом: какую задачу мы решаем?
Пример
Нам нужно актуализировать данные абонента в системе. В примере кукбука сервис решает задачу обновления данных вымышленного абонента. Если упростить:
- Есть текущий стэйт абонента.
- Есть внешняя система, из которой актуальные данные поступают в сервис «Актуальность».
- Он формирует запрос в сервис «Обновлялкин». Бизнес-логика тут такая, что сервис принимает решение, нужно ли обновлять данные, если нужно — формирует запрос на актуализацию данных и отправляет в систему «Абоненты».
- «Абоненты» — единая точка добавления и обновления абонентов. Эта система знает о взаимосвязях абонента с другими сущностями и то, за какие нити нужно подёргать на случай обновления атрибута, например, mobileRegionId.
Я в кукбуке опишу «Обновлялкин» — это наше связующее звено между «Абонентами» и «Актуальностью». Задача простая, если ты прокачался и правильно рисуешь границы.
Визуализация:
Граница ответственности Bounded Context-сервиса по обновлению данных — обновление данных. Он может запросить сервис «Абоненты», чтобы получить актуальные данные абонента, и может получить запрос на обновление от «Актуальности». Суть его бизнес-логики:
- Понять, что обновление необходимо по вошедшему запросу на обновление данных.
- Сформировать запрос на обновление.
- Отправить его в «Абоненты». Как обновлять атрибуты, «Абоненты» знает.
Так мы и опишем «Обновлялкин» в кукбуке.
Стратегические паттерны DDD — Bounded Context, Ubiquity Language — я описывать не буду, всё это найдёте по ссылкам в конце поста. Едем дальше.
Очень круто помнить о важности коммуникации, общего языка — в DDD 15 Years.
Наша задача при грамотно определённой границе сервиса и общем языке в команде — описать его верхнеуровнево в документации в конфлю. И так описать на Kotlin(Java), чтобы сам код был документацией. Сделать это на F# проще, но кровавый интерпайз не оставил нам выбора, Гарри.
Мы пишем на Kotlin.
Опишем в документации:
И в финале мы получим код, который описывает бизнес-логику:
Чем плохи долгие постановки в конфлю с кучей информации по имплементации от аналитика?
- Их нужно аналитить разработчику или снять ответственность и тупо кодить по аналитике. Второй подход мы исключаем. Первый нам не подходит: это дорого.
- Имплементация может измениться при рефакторинге — и вот у вас неактуальная документация. Для разработки это лишний гемор и усложнённый процесс, регламент, нужно следить. Кто хочет быть пастухом актуализации документации?
В идеале документация — в коде и ридмихах к коду. К этому мы и идём. Профит очевиден: код по бизнес-процессу, что я привёл выше, легко понять и сматчить с описанием функционала, поддерживаем это в одном месте, и актуальная документация у вас под рукой.
В кукбуке я привёл пример плохой документации и сценарий подхода к этому:
Далее по кукбуку необходимо перестроить мышление команды.
Думай моделями и бизнес-процессами, а не тем, как модель хранится в БД.
Забудьте Table-Driven Design (Database Oriented-мышление) — используйте только доменные объекты при обсуждении задачи. Не думайте о низкоуровневой реализации.
Не говорите: «Нам нужно обновить поле в таблице». Это вообще может быть не так, и процесс может быть намного сложнее.
Хорошо, мы договорились — общаемся объектами и процессами, становимся экспертами, описываем кратко суть задачи. Дело осталось за малым: описать всё в функциональной парадигме на Kotlin и покрыть юнит-тестами бизнес-логику.
Получить такой элегантный код нам помогут одна простая гениальная идея R.O.P и Type Driven Development при реализации тактических паттернов DDD (Aggragate, Value Object) и пара хороших паттернов, таких, как Rich Domain Model, Can Execute/ Execute.
Проклятие слабой доменной модели
В двух банках я видел и рос по такой структуре кода:
- /rest-пакет с контроллерами.
- /domain-пакет с Data Transfer Object (DTO).
- /services-пакет с сервисами, в которых хорошо или в стиле описана бизнес-логика.
И кругом — антипаттерн, слабая доменная модель. Что это такое? Это классы, контейнеры атрибутов, а имя им POJO в мире JVM.
Например:
Чем это плохо? Тем что при таком подходе вся бизнес-логика концентрируется в классах сервисов.
Я так жил почти всю программерскую жизнь и сервисы структурировал хорошо. Мы тестировали бизнес-логику в сервисах аккуратными интеграционными тестами, но в этом её недостаток. Каждый кейс — отдельный набор конфигураций для интеграционных тестов (например, wiremock) или mock. Это превращается в дополнительную кропотливую и злобную работу, а есть желание кода писать поменьше.
Заложил кривой паттерн.
В отчаянных руках агрессивных и продуктивных мидлов файлов конфигов может быть много (говорят, бывает больше 80). И вот вы уже не можете просто отрефакторить — нужно разобраться в конфигах, и если добавить что-то новое — добавить конфигураций заглушек. Это дерьмо лучше не трогать.
Чтобы тестировать логику юнит-тестами, необходимо изолировать доменную модель от интеграций.
Поможет в этом древний паттерн «Сильная доменная модель». Она содержит в себе бизнес-логику.
Например, Aggregate.
SubscriberDataUpdate
Класс обновления данных и его бизнес-логика таковы: сгенерировать запрос или сообщить нам, что обновление не требуется.
Логика по обновлению тут:
Про Aggregate и ValueObject хорошо написал Эрик Еванс и отдельно в видосах с Владимиром Хориковым и Ахтям Сакаевым в списке ниже, а также я отдельно выделил определение в кукбуке.
А визуализировать изоляцию доменной модели от интеграций хорошо помогает архитектура в стиле зубатого авокадо. Луковичная архитектура Onion Architecture-приложения:
В итоге мы получим изолированную сильную доменную модель от интеграций. Уровень сервисов приложений мы будем использовать как тупой поток.
Наша цель — простой и тупой сервис:
Чтобы добиться этого, нам необходимо следовать правилу: наша доменная модель должна быть всегда валидна и рождаться в приложении только благодаря фабричным методам.
А что делать, если модель не может возникнуть и мы должны выбросить ошибку? Мы не используем исключения, а применяем R.O.P. Скотт на своей страничке поясняет, что это название хорошо подходит для визуализации процесса.
Railway Oriented Programming — error handling in functional languages
На этой простой идее в работе с ошибками далее мы построим весь процесс.
Рождение сильной доменной модели, интеграции с внешними системами.
R.O.P.
Используем two track type Result<Data, Error> на выходе у функции, если она может зафэйлиться — чаше всего интеграции и фабричные методы доменной модели.
На вход в самом просто случае может прийти класс, как в случае с фабричным методом emerge ValueObject SubscribertId.
SubscribertId — не пустая строка, которая содержит только цифры и длина которой не превышает шести.
Мы используем самые важные строительные блоки DDD — тактический паттерн Value Object, который может возникнуть только благодаря фабричным методам.
А если ValueObject не может возникнуть по бизнес-логике — мы без исключения (Exception) возвращаем Two Track Type:
И тут же раскрывается сила Type Driven Development: в ядре нашей доменной модели нет места примитивам. Все строительные блоки содержат в себе релевантную бизнес-логику ограничений, и их просто протестировать.
Тут нам нужно немного перестроить мышление, чтобы всё сложилось от входа на REST до выхода на REST.
Поможет следующая старая мысль: любой бизнес-процесс мы можем описать в функциональном стиле а-ля unix pipe.
Декомпозируем эту мысль на задачу реализации рест-сервиса от входного запроса до результата. Мы можем без примитивов в сердце доменной модели (классов) описать в функциональном стиле переход алгебраических типов из одного в другой следующим образом:
Что у нас приходит на вход в REST-ресурс? Непроверенный запрос на обновление — UnvalidatedDataUpdateRequest.
Описываем:
Далее мы представляем Happy Path. И непроверенный запрос на обновление превращается в валидный запрос на обновление.
ValidatedDataUpdateRequest
А если непроверенный запрос содержит в себе ошибку — фабричный метод ValidatedDataUpdateRequest.emerge -> вернёт нам Result
И теперь всю последовательность алгебраических типов мы можем представить в виде пайпа, где каждый последующий возникает на основе предыдущего (Scott Wlaschin).
Если на каком-то участке цепи возникает ошибка — мы её проталкиваем далее по пайпу R.O.P. благодаря Two Track Type Result<Data, Error>.
Остановимся немного на входе в рест-ресурс и обратим внимание, что валидация уходит у нас на уровень доменных классов и становится частью доменной модели, а не расползается по приложению в эксепшен-хендлеры и прочие вспомогательные слои.
Бизнес-логика находится там, где ей и место, а сам код, описывая её, превращается в документацию.
Такой код просто тестировать юнит-тестами, и, что немаловажно, любое изменение в бизнес-логике в иммутабельном классе приведёт к так называемой ряби ошибок времени компиляции. Добавил поле в класс — ошибка выскочила на уровне компиляции. Изменил ограничение — ошибку отобьёт нам тест.
Пример простого ValueObject DataUpdateId:
Пруфпик выше подтверждает: мы легко можем прочитать ограничения по бизнес-логике, и такой класс легко протестировать.
Пример элегантного барона теста ниже:
Иммутабильность и валидность доменной модели
Чем плохи невалидные мутабельные классы в сердце домена? Они с ноги порождают проблемы проверки на null и проверки валидности: больше кода нужно писать.
Уф! Самое важное обсудили.
R.O.P. преисполнились — нам осталось запустить наше доменное ядро по сервису с интеграциями без исключений и всё это протестировать.
Цель — получить код, описывающий бизнес-логику сервиса.
На каждом шаге пайпа транспортного уровня сервиса функция получает:
На входе Result<Data, Error> отрабатывает логику, опрашивая доменную модель, а если на вход пришла ошибка — возвращает сразу эту ошибку.
Например, если на этапе поиска текущего статуса абонента findSubscriberForUpdate у нас ошибка уровня интеграции — это Result, на выходе функции findSubscriberByRest.
Любая функция, что может зафэйлиться, возвращает Result
Таким образом, R.O.P. как идея пронизывает весь дизайн приложения. REST Gateway возвращает также Result или Mono<Result> whatever.
Паттерн CanExecute/Execute в данном случае в функции findSubscriberForUpdate отрабатывает на входе как функция fold (fold семейство функций высшего порядка).
Суть паттерна CanExecute/Execute в названии — спроси доменный класс, в нашем случае Result — можно ли исполнить дальнейший шаг бизнес-логики и найти актуальные данные по абоненту вызовом findSubscriberForUpdate). Если нет — тогда вернуть ошибку, что пришла на вход.
Получаем часть рецепта кукбука.
Запусти иммутабельную всегда валидную доменную модель по транспортному тоннелю «бизнес-процесс» без исключений, на шлюзах при сбоях вам помогут two track type Result<Data, Error> и canExecute/execute.
Фух! Погнали дальше, кратко обсудим тесты, и по коду сразу можно просмотреть основные паттерны юнит-тестов.
Юнит-тестирование
Я обожаю тесты! Мне нравятся классический TDD и Red Green Refactoring, но в нашем стремительном темпе я пришёл к практике: сначала часть кода — потом тесты на эту часть.
Не всё сразу на этапе прототипирования и моделирования по кукбуку удобно начинать с тестов. Суть простая: описал часть бизнес-логики — написал тесты. Коммит содержит в себе в идеале и классы, и тесты. Хочешь по классике — давай. На мой взгляд, классический TDD — самый весёлый и захватывающий процесс и игра.
Я на своей шкуре как яростный адвокат тестов прочувствовал, как круто тесты помогают в разработке более простого дизайна кода. Но я прочувствовал и то, насколько неправильные тесты могут быть токсичны для проекта и бизнеса.
Выше я приводил пример злых кусающихся интеграционных тестов. Что мы делаем в своём строгом кунг-фу с тестами — будет дальше. Мне поможет мысль: чего мы точно не хотим делать — это писать больше кода.
Значит, мы отказываемся от mock’ов в тестах ну или сводим их к минимуму. Интеграции (рест-шлюзы, рест-клиенты) проверяем одним интеграционным тестом, тут mock используем. Чем плохи mock’и:
- Они концентрируют разработчика на деталях имплементации, а не на выходных параметрах.
- Их нужно поддерживать и при рефакторинге — это дополнительная работа.
Мы идём трудной дорожкой классической школы тестирования и возводим тестирование «чёрного ящика» и тестирование выходного параметра функции в абсолют.
Максимум тестов сконцентрирован на бизнес-логике в простых юнит-тестах, а тривиальный код сервиса мы тестируем одним интеграционным тестом.
Если рука бойца колоть устала — можно покрыть интеграционным тестом rest-сервис. Он обходится нам дорого: нужна mock-конфигурация или надо поднимать контекст спринга. При этом он за один тест покроет максимальное количество кода: rest-контроллер, сервис приложения и доменных классов, конфигурации.
Пример достойного интеграционного теста:
Самый кайф — все крайние точки и всю бизнес-логику мы тестируем юнит-тестами от ValueObject до агрегатов.
Когда у меня всё это получилось, я помню этот момент светлой радости внутри и ликования: так можно, и это работает! )
Простой тест ValueObject мы разобрали выше. Разберём кратко тест агрегата по паттерну AAA:
Этот агрегат можно улучшить) — включить бизнес-логику на подготовку запроса для обновления в фабричный метод минус класс.
Погнали по деталям:
- Подготавливаем валидные DTOʼшки (строки 13–23).
- Воссоздаём состояние на пайпе сервиса SubscriberDataUpdateService (строка 26 скрина ниже).
- Успешного вызова функции findSubscriberByRest (строка 42 скрина ниже).
В тесте это выглядит так (строка 28 скрина ниже):
Следовательно, System Under Test -> sut:
В точности моделирует конкретное состояние системы:
На вход приходят валидные доменные классы DataUpdate и Subscriber, и у них отличается поле mobileRegionId (строки 17, 22 ниже на скрине).
И мы тестируем в SUT SubscriberDataUpdate суть его бизнес-логики.
Подготовка запроса на обновление абонента:
Такой тест соответствует прекрасным оценкам по четырём аспектам хороших юнит-тестов:
- Защита от багов.
- Устойчивость к рефакторингу.
- Быстрая обратная связь.
- Простота поддержки.
Получаем финальный ингредиент кукбука:
- Классическая школа тестирования.
- Прагматичный набор тестов, сфокусированный на бизнес-логике в валидных иммутабильных классах.
- Возводим в абсолют тестирование чёрной коробки — тестирование выходных данных функции. Интеграционных тестов минимум — хватает одного теста на Happy Path на REST in: out.
При таком подходе к дизайну кода мы получаем на выходе качественное покрытие тестами алгебраических типов, которые описывают бизнес-логику. Мы спокойны.
А ещё мы ровно, как по циркулю, отстраиваем пирамиду тестирования. Только представьте, как это всё красиво может считаться с E2E автотестами!
Прежде чем записать рецепт, обратите внимание: кукбук очень красиво вплетается в функциональную парадигму благодаря стремлению получить максимум юнит-тестов на выходные параметры функций. Скотт, спасибо за R.O.P.
Мы получаем понятный строгий дизайн и паттерн в переходах на монадах, честные и чистые функции и, что суперважно, — реактивный понятный код.
Я видел последствия функциональных ужасов очередной кибер-бойни загончика для миньонов № 5 — уверен, вы видели тоже, и ПТСР реактивного кода на 30–50 непонятных строк пережить очень трудно.
Это просто боль, когда вместо того, чтобы затащить на прод фичу, нужно три часа разбираться в реактивной функциональной кровавой лапше, гранулированной по SOLID с размазанной бизнес-логикой на уровнях сервисов приложений. А потом ещё три часа придумывать, как это всё протестировать и не сломать тесты по соседству. Не надо так. Теперь всё будет строго.
Рецепт:
- Станьте экспертом предметной области — разберитесь, что и как должно работать на всех уровнях. Ваш код — ваша ответственность. И помните: качественная разработка — это результат качественной коммуникации.
- Опишите в функциональном стиле бизнес-процесс с доменными классами.
- Реализуйте в функциональном стиле всегда валидную богатую доменную модель без примитивов.
- Покройте юнит-тестами бизнес-логику, которая содержится в доменной модели.
- Запустите доменную модель по тоннелю «бизнес-процесс» без исключений, на шлюзах помогут two track type Result<Data, Error> и canExecute/execute.
Если готовите по рецепту — можно получить в качестве результата:
- Простую строгую структуру приложения — хороший дизайн кода в функциональным стиле.
- Код будет оснащён эффективным набором простых юнит-тестов, которые сфокусированы на изолированной от интеграций бизнес-логике.
- Количество интеграционных тестов сведено к достаточному минимуму.
- Интеграционные тесты более дорогие в сопровождении и поддержке.
- Mock не используются вообще или в крайне исключительных ситуациях.
Собираюсь со всем этим на конфе выступить — не раз и не два получал позитивный фидбэк, и хочется нарытым поделиться. Апрель Jpoint 2023.
This is The Way.
Что дальше, Максим? Добавлю BDD в эту историю.
P. S. Это один из возможных работающих путей, не пуля и не кол.
Но если быть откровенным, в моих задачах этот кукбук помог загнать серебряную пулю в сердце хаоса в качестве кода.
Footnotes:
There is no I in Software Craftsmanship
Книга: Domain Modeling Made Functional
Книга: Принципы юнит-тестирования
Книга: Domain-Driven DesignThe First 15 Years
Видео: Scott Wlaschin — Railway Oriented Programming — error handling in functional languages
Видео: Владимир Хориков — Domain-driven design: Cамое важное
Видео: Ахтям Сакаев — DDDamn good!
Vladimir Khorikov, Refactoring from Anemic Domain Model Towards a Rich One