Как я делал внутренний cookbook по тому, как писать код (и результат можно скачать)

c5blxhmztxbiwvxfbxspryqyvwc.png
Авокадо с зубами подсказывает, что так код легче поддерживать, дописывать и рефакторить. Мы всё теперь пишем только так.

Привет, Хабр! У нас была проблема: каждый писал код как хотел. Было очень тяжело это поддерживать и ревьюить. Мы сначала думали, что достаточно написать стандарт кода. Оказалось, недостаточно, ему ещё надо обучить. Чтобы обучить, мы открыли для ревью эталоны кода, чтобы покрыть ими самую частую логику взаимодействия с компонентами. Тоже не хватило. А заодно я узнал, что мои же «золотые» образцы противоречили моему же стандарту кода (сначала было смешно, а потом пришлось переписывать).

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

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

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

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

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

Посвящается всем, кто коллекционирует элегантные решения без привязки к языку, фрэймворку, Фаундлингам и Software Craftsmanʼам.

Погнали.

Самое главное


Вот ссылка на опубликованный CookBook: https://git.codemonsters.team/guides/ddd-code-toolkit/src/branch/dev

Почему не хватило стандартов разработки?


Потому что в моих исходных кодах по стандартам содержались предпосылки к ошибкам. Вот пример:

cd0lpz3smp2guubtmlygmmdezhq.png
Сервис с интеграциями и бизнес-логикой

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

Например, мы используем Domain Driven Design приложений, что очень хорошо защищает от расползания логики по тем местам, где её в идеале быть не должно.

Cила этой истории в том, что она собрала много полезных паттернов вместе, и это очень хорошо работает.

odxmz9p6re3dlpwe_mwsuqxknxw.png

Впрочем, давайте прямо с самого начала.

Кукбук: зачем он нужен


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

Кукбук содержит в себе паттерны применения стратегических и тактических паттернов DDD, Type Driven Development, Reach Domain Model, запущенных в функциональной парадигме с прагматичными юнит-тестами бизнес-логики без исключений по паттерну R.O. P (Railway Oriented Programming).

Всё это помогает:

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


Временами после работы я рублюсь на Flutter + kotlin backend и радуюсь, что подходы из кукбука — как неокиберпоэзия: работают на всех уровнях — от тупого фронта до хитроумного мидла с бэком.

Вся эта история проверена на практике промом и совершенно разными командами и задачами.

Как он появился


За 2,5 года в роли тимлида и техлида разработки, отвечающего как за качество delivery, так и за качество кода, я с командами затащил один проект в прод, и сейчас катим, как танки, второй.

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

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

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

Обрисую постфактум мою задачу:

hgca_a1rmilrxibfzcpqgqkox4m.png

Без алкоголя сложно порой высвободить внутреннего Паланика, Хэмингуэя или Аллена. Погнали дальше, Максим.

Благодаря крутым работам инженеров Скота Влашина, Владимира Хорикова, Роберта Мартина, мадам Рефлексии и поискам удалось собрать воедино кукбук по реализации сервиса с бизнес-логикой (агрегирующие сервисы, перекладчики и прочие вспомогательные истории веб-разработки он также покрывает).

Новичку он призван показать, насколько важны методологии DDD, TDD (прагматичные тесты), как прекрасен Type Driven Development, как необходима грамотная коммуникация, что в функциональной парадигме можно понятно и просто писать. Можно и без неё. И что суперважно быть экспертом в той области, которую моделируешь.

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

Это достаточно жёсткое кунг-фу, пропитанное олдскульной классической школой тестирования, сильной доменной моделью и функциональной парадигмой.

Просто делайте по кукбуку тикет — будет хорошо, и начинайте изучать подробнее кирпичики, без которых его не выстроить. Баста!

Когда возникают проблемы, можно было описать пару template-сервисов и сказать: «Делайте так!» —, но чтобы грамотно определить место бизнес-логике, без DDD не обойтись.

Интуиция, грамотные решения, руководитель разработки, долгие размусоленные встречи про то, какой сервис что делает и где какая бизнес-логика, — это всё круто, и за это (в том числе) нам банки платят. А ещё можно две недели одну кнопку в спринт затаскивать — всякое бывает.

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

Нужна история более строгая и масштабируемая.

Кукбук — как раз об этом: как превратить реактивный код в документацию и юнит-тестами покрыть бизнес-логику.

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

Если вас не впирает мой стиль повествования — кукбук сразу по ссылке в конце поста, прокручивайте вниз, берите и пользуйтесь.

Для въедливых я немного пройдусь по каждому из разделов.

DDD — самое главное


Мы начали с того, что разработчик становится экспертом в той области, которую он описывает, и много внимания уделяем общему языку. Применяем паттерн Ubiquity Language.

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

Качественная коммуникация — рабочее взаимодействие экспертов разных предметных областей, идущих к единой цели совместными усилиями.

Я видел истории про страдающих и за всё отвечающих аналитиков, такие истории до добра не доводят. Я знаю, что есть команды, где аналитики всё за всех решают и говорят, как всё должно работать. Ещё и артефакты катят в пром. А разработчики сидят за удобной оградкой — ждут спецификации, и чуть что: «У меня лапки, мне так аналитики сказали».

В стагнирующем проекте такой подход работает и помогает коротать срок в период выплат ипотеки. У нас не так в командах.

Пока разработчик не поймёт, что нужно разработать и как это работает на всех слоях от кнопки до бэка, — ничего путного не разработает.

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

Твой код — твоя зона ответственности. Мы обсуждаем задачу и просто описываем, что нужно сделать. Не разбираемся с талмудами аналитики, которая приходит как постановка, а просто задаёмся вопросом: какую задачу мы решаем?

Пример

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

  • Есть текущий стэйт абонента.
  • Есть внешняя система, из которой актуальные данные поступают в сервис «Актуальность».
  • Он формирует запрос в сервис «Обновлялкин». Бизнес-логика тут такая, что сервис принимает решение, нужно ли обновлять данные, если нужно — формирует запрос на актуализацию данных и отправляет в систему «Абоненты».
  • «Абоненты» — единая точка добавления и обновления абонентов. Эта система знает о взаимосвязях абонента с другими сущностями и то, за какие нити нужно подёргать на случай обновления атрибута, например, mobileRegionId.


Я в кукбуке опишу «Обновлялкин» — это наше связующее звено между «Абонентами» и «Актуальностью». Задача простая, если ты прокачался и правильно рисуешь границы.

Визуализация:

-scy3yfidqgmnb8ujxmh89bxd-g.png

Граница ответственности Bounded Context-сервиса по обновлению данных — обновление данных. Он может запросить сервис «Абоненты», чтобы получить актуальные данные абонента, и может получить запрос на обновление от «Актуальности». Суть его бизнес-логики:

  • Понять, что обновление необходимо по вошедшему запросу на обновление данных.
  • Сформировать запрос на обновление.
  • Отправить его в «Абоненты». Как обновлять атрибуты, «Абоненты» знает.


Так мы и опишем «Обновлялкин» в кукбуке.

Стратегические паттерны DDD — Bounded Context, Ubiquity Language — я описывать не буду, всё это найдёте по ссылкам в конце поста. Едем дальше.

Очень круто помнить о важности коммуникации, общего языка — в DDD 15 Years.

Наша задача при грамотно определённой границе сервиса и общем языке в команде — описать его верхнеуровнево в документации в конфлю. И так описать на Kotlin (Java), чтобы сам код был документацией. Сделать это на F# проще, но кровавый интерпайз не оставил нам выбора, Гарри.

Мы пишем на Kotlin.

Опишем в документации:

zx0gequ7eh1bim6scoa3pj-cv34.png

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

y30xdiqysylf-pod68kmgzbzlnw.png

Чем плохи долгие постановки в конфлю с кучей информации по имплементации от аналитика?

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


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

В кукбуке я привёл пример плохой документации и сценарий подхода к этому:

mimtdfmc7mxsbmlapl6xrtoecfe.png

Далее по кукбуку необходимо перестроить мышление команды.

Думай моделями и бизнес-процессами, а не тем, как модель хранится в БД.

Забудьте 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.

Например:

9xt9qnty6keojx0eiit2nufvrmo.png

Чем это плохо? Тем что при таком подходе вся бизнес-логика концентрируется в классах сервисов.

sk9cgpwxo3u5hxzcdjwwfsgdpmw.png

Я так жил почти всю программерскую жизнь и сервисы структурировал хорошо. Мы тестировали бизнес-логику в сервисах аккуратными интеграционными тестами, но в этом её недостаток. Каждый кейс — отдельный набор конфигураций для интеграционных тестов (например, wiremock) или mock. Это превращается в дополнительную кропотливую и злобную работу, а есть желание кода писать поменьше.

Заложил кривой паттерн.

y1swyikltprddc08ywkjgzi22xg.png

sra5cwkcs7vzogmyavudxqfhrhk.png

В отчаянных руках агрессивных и продуктивных мидлов файлов конфигов может быть много (говорят, бывает больше 80). И вот вы уже не можете просто отрефакторить — нужно разобраться в конфигах, и если добавить что-то новое — добавить конфигураций заглушек. Это дерьмо лучше не трогать.

Чтобы тестировать логику юнит-тестами, необходимо изолировать доменную модель от интеграций.

Поможет в этом древний паттерн «Сильная доменная модель». Она содержит в себе бизнес-логику.

Например, Aggregate.

SubscriberDataUpdate
zc0urjdrknqtlqrrpeiatobh4c4.png


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

vd2nku7qsae_laxvb3gveycl6-8.png

Логика по обновлению тут:

ui2ybi5ensuqw9lp7megff9vpd4.png

Про Aggregate и ValueObject хорошо написал Эрик Еванс и отдельно в видосах с Владимиром Хориковым и Ахтям Сакаевым в списке ниже, а также я отдельно выделил определение в кукбуке.

А визуализировать изоляцию доменной модели от интеграций хорошо помогает архитектура в стиле зубатого авокадо. Луковичная архитектура Onion Architecture-приложения:

c5blxhmztxbiwvxfbxspryqyvwc.png

В итоге мы получим изолированную сильную доменную модель от интеграций. Уровень сервисов приложений мы будем использовать как тупой поток.

Наша цель — простой и тупой сервис:

c0o48aux3pidp21ylyb1kvg8h8k.png

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

А что делать, если модель не может возникнуть и мы должны выбросить ошибку? Мы не используем исключения, а применяем R.O. P. Скотт на своей страничке поясняет, что это название хорошо подходит для визуализации процесса.

Railway Oriented Programming — error handling in functional languages

На этой простой идее в работе с ошибками далее мы построим весь процесс.

Рождение сильной доменной модели, интеграции с внешними системами.
R.O. P.

e-vyje8rymwkuu3sn73io_zdy4k.png

Используем two track type Result на выходе у функции, если она может зафэйлиться — чаше всего интеграции и фабричные методы доменной модели.

На вход в самом просто случае может прийти класс, как в случае с фабричным методом emerge ValueObject SubscribertId.

SubscribertId — не пустая строка, которая содержит только цифры и длина которой не превышает шести.

e-sgitg93fn911b95-errs5_els.png

Мы используем самые важные строительные блоки DDD — тактический паттерн Value Object, который может возникнуть только благодаря фабричным методам.

А если ValueObject не может возникнуть по бизнес-логике — мы без исключения (Exception) возвращаем Two Track Type:

wzq0nhinejbxpnvqw82yy7m5ssm.png

И тут же раскрывается сила Type Driven Development: в ядре нашей доменной модели нет места примитивам. Все строительные блоки содержат в себе релевантную бизнес-логику ограничений, и их просто протестировать.

Тут нам нужно немного перестроить мышление, чтобы всё сложилось от входа на REST до выхода на REST.

Поможет следующая старая мысль: любой бизнес-процесс мы можем описать в функциональном стиле а-ля unix pipe.

zoz_vqf8xdeccsvaszj02qt6z7e.png

Декомпозируем эту мысль на задачу реализации рест-сервиса от входного запроса до результата. Мы можем без примитивов в сердце доменной модели (классов) описать в функциональном стиле переход алгебраических типов из одного в другой следующим образом:

Что у нас приходит на вход в REST-ресурс? Непроверенный запрос на обновление — UnvalidatedDataUpdateRequest.

Описываем:

a61_rahkrkbldgi-vwmdjj-dxy0.png

Далее мы представляем Happy Path. И непроверенный запрос на обновление превращается в валидный запрос на обновление.

ValidatedDataUpdateRequest

jxc-tvgawd838v25yke3w5qig7k.png

А если непроверенный запрос содержит в себе ошибку — фабричный метод ValidatedDataUpdateRequest.emerge → вернёт нам Result

И теперь всю последовательность алгебраических типов мы можем представить в виде пайпа, где каждый последующий возникает на основе предыдущего (Scott Wlaschin).

9_e6x83w1mv1qwcv5quk-wcaeua.png

Если на каком-то участке цепи возникает ошибка — мы её проталкиваем далее по пайпу R.O. P. благодаря Two Track Type Result.

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

Бизнес-логика находится там, где ей и место, а сам код, описывая её, превращается в документацию.

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

Пример простого ValueObject DataUpdateId:

skv-9uixttjl6xhixlwizo7vhh4.png

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

Пример элегантного барона теста ниже:

2cssdqa7u41xl6ekxibvv_texzu.png

Иммутабильность и валидность доменной модели


Чем плохи невалидные мутабельные классы в сердце домена? Они с ноги порождают проблемы проверки на null и проверки валидности: больше кода нужно писать.

oxcgubbdyxtsygffmwuawgt94zi.png

Уф! Самое важное обсудили.

R.O. P. преисполнились — нам осталось запустить наше доменное ядро по сервису с интеграциями без исключений и всё это протестировать.

Цель — получить код, описывающий бизнес-логику сервиса.

b6oi3nys8pqoa5ykabvaluh_xem.png

На каждом шаге пайпа транспортного уровня сервиса функция получает:

На входе Result отрабатывает логику, опрашивая доменную модель, а если на вход пришла ошибка — возвращает сразу эту ошибку.

Например, если на этапе поиска текущего статуса абонента findSubscriberForUpdate у нас ошибка уровня интеграции — это Result, на выходе функции findSubscriberByRest.

8zuxvjzq6rwuqpqsxcia8epvjbg.png

Любая функция, что может зафэйлиться, возвращает Result

Таким образом, R.O. P. как идея пронизывает весь дизайн приложения. REST Gateway возвращает также Result или Mono whatever.

Паттерн CanExecute/Execute в данном случае в функции findSubscriberForUpdate отрабатывает на входе как функция fold (fold семейство функций высшего порядка).

Суть паттерна CanExecute/Execute в названии — спроси доменный класс, в нашем случае Result — можно ли исполнить дальнейший шаг бизнес-логики и найти актуальные данные по абоненту вызовом findSubscriberForUpdate). Если нет — тогда вернуть ошибку, что пришла на вход.

dqru7onvid4rh2xuwlutohmyowi.png

Получаем часть рецепта кукбука.

Запусти иммутабельную всегда валидную доменную модель по транспортному тоннелю «бизнес-процесс» без исключений, на шлюзах при сбоях вам помогут two track type Result и canExecute/execute.

Фух! Погнали дальше, кратко обсудим тесты, и по коду сразу можно просмотреть основные паттерны юнит-тестов.

Юнит-тестирование


Я обожаю тесты! Мне нравятся классический TDD и Red Green Refactoring, но в нашем стремительном темпе я пришёл к практике: сначала часть кода — потом тесты на эту часть.

Не всё сразу на этапе прототипирования и моделирования по кукбуку удобно начинать с тестов. Суть простая: описал часть бизнес-логики — написал тесты. Коммит содержит в себе в идеале и классы, и тесты. Хочешь по классике — давай. На мой взгляд, классический TDD — самый весёлый и захватывающий процесс и игра.

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

Выше я приводил пример злых кусающихся интеграционных тестов. Что мы делаем в своём строгом кунг-фу с тестами — будет дальше. Мне поможет мысль: чего мы точно не хотим делать — это писать больше кода.

Значит, мы отказываемся от mock«ов в тестах ну или сводим их к минимуму. Интеграции (рест-шлюзы, рест-клиенты) проверяем одним интеграционным тестом, тут mock используем. Чем плохи mock«и:

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


Мы идём трудной дорожкой классической школы тестирования и возводим тестирование «чёрного ящика» и тестирование выходного параметра функции в абсолют.

Максимум тестов сконцентрирован на бизнес-логике в простых юнит-тестах, а тривиальный код сервиса мы тестируем одним интеграционным тестом.

yn2xcumfdhy0lmlztuaczd9hvp8.png

Если рука бойца колоть устала — можно покрыть интеграционным тестом rest-сервис. Он обходится нам дорого: нужна mock-конфигурация или надо поднимать контекст спринга. При этом он за один тест покроет максимальное количество кода: rest-контроллер, сервис приложения и доменных классов, конфигурации.

Пример достойного интеграционного теста:

8vb_izk2zyp5bys5xdo60axo9hy.png

Самый кайф — все крайние точки и всю бизнес-логику мы тестируем юнит-тестами от ValueObject до агрегатов.

Когда у меня всё это получилось, я помню этот момент светлой радости внутри и ликования: так можно, и это работает!)

Простой тест ValueObject мы разобрали выше. Разберём кратко тест агрегата по паттерну AAA:

v3gmgxgr4c9seputdsalcgw6_sa.png

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

Погнали по деталям:

  1. Подготавливаем валидные DTOʼшки (строки 13–23).
  2. Воссоздаём состояние на пайпе сервиса SubscriberDataUpdateService (строка 26 скрина ниже).
    oyf_rkbgiprgosmldhl2rokru-y.png
  3. Успешного вызова функции findSubscriberByRest (строка 42 скрина ниже).

    sb_okkivdpytojg0rtbjlnejbf4.png


В тесте это выглядит так (строка 28 скрина ниже):

pvqjenyj2gfddstn1ehb2ym3zo4.png

Следовательно, System Under Test → sut:

1ui_rsgqvxqaczc1jr0fjlskkzw.png

В точности моделирует конкретное состояние системы:

На вход приходят валидные доменные классы DataUpdate и Subscriber, и у них отличается поле mobileRegionId (строки 17, 22 ниже на скрине).

uuno8s3uv-qsz3-hkaw1jjgmdao.png

И мы тестируем в SUT SubscriberDataUpdate суть его бизнес-логики.

Подготовка запроса на обновление абонента:

3olfvnmgtp9khj4po0l7d7pev9s.png

Такой тест соответствует прекрасным оценкам по четырём аспектам хороших юнит-тестов:

  1. Защита от багов.
  2. Устойчивость к рефакторингу.
  3. Быстрая обратная связь.
  4. Простота поддержки.


Получаем финальный ингредиент кукбука:

  • Классическая школа тестирования.
  • Прагматичный набор тестов, сфокусированный на бизнес-логике в валидных иммутабильных классах.
  • Возводим в абсолют тестирование чёрной коробки — тестирование выходных данных функции. Интеграционных тестов минимум — хватает одного теста на Happy Path на REST in: out.


При таком подходе к дизайну кода мы получаем на выходе качественное покрытие тестами алгебраических типов, которые описывают бизнес-логику. Мы спокойны.

А ещё мы ровно, как по циркулю, отстраиваем пирамиду тестирования. Только представьте, как это всё красиво может считаться с E2E автотестами!

Прежде чем записать рецепт, обратите внимание: кукбук очень красиво вплетается в функциональную парадигму благодаря стремлению получить максимум юнит-тестов на выходные параметры функций. Скотт, спасибо за R.O. P.

Мы получаем понятный строгий дизайн и паттерн в переходах на монадах, честные и чистые функции и, что суперважно, — реактивный понятный код.

Я видел последствия функциональных ужасов очередной кибер-бойни загончика для миньонов № 5 — уверен, вы видели тоже, и ПТСР реактивного кода на 30–50 непонятных строк пережить очень трудно.

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

Рецепт:

  1. Станьте экспертом предметной области — разберитесь, что и как должно работать на всех уровнях. Ваш код — ваша ответственность. И помните: качественная разработка — это результат качественной коммуникации.
  2. Опишите в функциональном стиле бизнес-процесс с доменными классами.
  3. Реализуйте в функциональном стиле всегда валидную богатую доменную модель без примитивов.
  4. Покройте юнит-тестами бизнес-логику, которая содержится в доменной модели.
  5. Запустите доменную модель по тоннелю «бизнес-процесс» без исключений, на шлюзах помогут two track type Result и 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

© Habrahabr.ru