Information
- Rating
- 527-th
- Location
- Кольцово, Новосибирская обл., Россия
- Date of birth
- Registered
- Activity
Specialization
Chief Technology Officer (CTO), Software Architect
Lead
From 500,000 ₽
Functional programming
Object-oriented design
Design information systems
TDD/BDD
Kotlin
PostgreSQL
Java Spring Framework
Linux
Git
Docker
А, и собственно HTTP- запрос я заворачиваю в хелпер, поэтому если меняется только синтаксис, а не семантика (например параметр из пути в запрос уезжает), то это тест-кейсы не затрагивает.
Это, наверное, зависит от контекста.
Я обычно перед тем как писать какой-то код, проектирую и согласовываю АПИ с фронтом. И тесты пишу через это АПИ. А если там будут существенные изменения, то модификация тестов - будет меньшей из проблем:)
Я работаю через ТДД и там где есть возможность пытаюсь рефлексировать стоит ли оно того.
Пока набралось два кейса:
Сравнение трудозатрат на первоначальную разработку и её полный реинжиниринг
Субъективное ощущение продакта на разработку большой фичи (человеко-год) по ТДД в сравнении с другими фичами в том же проекте. Тут ретру пока не публиковал
И в обоих кейсах разработка по ТДД была не медленнее разработки без тестов и команда допуска в 2-4 раза меньше багов.
Ну то есть с ТДД можно шипать фичи с той же скоростью, но при этом спокойно спать и в целом меньше стрессовать:)
Спасибо, хорошая и нужная статья.
Только я бы добавил про
У меня последние лет 5 в куче разных проектов (среди прочих - автоматизация выделенного бизнес-процесса крупного ритейлера, медтех, автоматизация работы юридического департамента) 90% "причины по которой я пишу ПО" - положить данные в РСУБД. И поэтому 90% кода тестировать в изоляции от внешнего мира особого смысла нет. В общем, на мой взгляд, изоляция внешних зависимостей - не универсальное правило и нужна только в сложных предметных областях (я таких не видел, но предполагаю, что к ним относится банкинг, страхование, логистика, e-commerce).
А в остальных случаях эффективнее по соотношению трудозатраты/(скорость + качество разработки), писать тесты как минимум с реальным PostgreSQL на RAM-диске.
И если заморочиться, такие тесты будут не критично медленнее - до 10 секунд на запуск 1 теста, и по 50мс в среднем на тест при запуске всего набора.
В остальном - всё плюсую.
Спасибо:)
List<Any?>
- является подтипомAny
."оператор" + - это на самом деле просто синтаксический сахар для создания копии мапы и добавления в неё элемента (копирование вместо добавления в имеющуюся - из-за ФП головного мозга)
Соответственно:
Метод
groupBy
возвращаетTable
akaList<Map<String, Any>>
Лямбда, которая передаётся в
rowGroups.map
возвращаетMap<String, List<Any?>>
А сам метод map возвращает
List<Map<String, List<Any?>>>
И при возврате из метода
groupBy
выполняется upcast (приведение к супертипу)List<Map<String, List<Any?>>>
->List<Map<String, Any>>
@razon
И как вы находите границы микросервисов и ограниченных контекстов?
Условно ко мне пришёл продакт, говорит хочу фичу логистики, она должна делать то-то и то-то - в результате какого процесса у меня появятся артефакты/термины "ПВЗ Customer", "ПВЗ Маппер" и т.д. и "Ограниченный контекст ПВЗ", "Ограниченный контекст Геоданные" и т.д.
@razon
Поясните, пожалуйста - куда и на каком уровне абстракции фронт встраивается в вашей модели?
В моей практике фронтовые приложения/компоненты зачастую работают со множеством (всеми) слабосвязанных между собой компонентов бэка и, кажется, сломают всю стройную картину - если фронт засунуть в один из ограниченных контекстов - он его coupling убьёт, а если в отдельный - у него самого ещё хуже coupling будет.
Так ни я в примечании к этому посте, ни Хориков в посте, на который я ссылаюсь, не пишем, что от моков надо полностью отказаться.
Я пишу, что отношение тестов без моков к тестам с моками может быть 100 к 1. То есть без моков никак - бывает в одном случае из ста. В моей практике последних 4 лет - 9 коммерческих проектов суммарно на ~100К строк Котлин-кода.
При этом среднее время теста по моим проектам - порядка 50мс:
Тут ещё, пожалуй, стоит пояснить, что под "моками" я понимаю именно мокирование собственных типов с помощью условного Mockito. А против условных WireMock, GreenMail и т.п. я ничего не имею, включая верификацию вызовов - это часть наблюдаемого (снаружи) повдениия системы, которая должна быть специфицирована в тестах.
Добавлю ещё, что эти тезисы подтверждаются фактами из моей практики
В моём предыдущем проекте с тестами без моков за 500 человеко-часов, которые я рассматривал в ретроспективе было 0.5 бага на задачу или 0.05 бага/человеко-час (подробности).
В текущем проекте за примерно человеко/год разработки нашли 2 бага (не соответствие поведения софта требованиям) и 0 регрессий. Проект ещё не завершён, поэтому ретру не проводил и точных цифр пока нет.
В этом же проекте с 93% покрытием кода тестами без моков я недавно сделал пару серьёзных изменений в модели и рефакторингов: сделал один из агрегатов частью другого, ввёл разделение доменной модели и модели персистанса, добавил версионирование частей агрегата (условно таблички и её строк) - в ходе этой работы не было внесено ни одной регрессии.
А рефакторинг (20 затронутых файлов файлов, 482 добавленных строки, 172 удалённых строки) не содержал ни одной строки изменений в тестах.
Это красноречивая иллюстрация того, что хорошие тесты проверяют поведение системы, а не её реализацию и, как следствие, не меняются при рефакторинге - изменении структуры кода, без изменения его поведения.
Я удивлён, что коммент выше заминусили в свете того, что все классики TDD предостерегают от повсеместного использования моков.
Оставлю здесь мудрость древних для будущих поколений:
Я пожалуй даже код левого блока оркистрации скину:)
- кажется, он очень похож на ваш пример кода и, похож на ваш пример с созданием версии - тут как раз создаётся новая версия некой (хитрой по сути и динамически настраиваемой по структуре) таблицы.
Ещё так попробую проиллюстрировать свою точку зрения.
Разделением ИО и логики я борюсь с этим:
Легенда:
Синие блоки - оркестрация, блоки которые взывают более одного типа других блоков и имеют когнитивную сложность <= 4
Красные блоки - ввод, блоки которые делают ввод и имеют когнитивную сложность <= 4
Зелёные блоки - бизнес-логика, блоки, которые не вызывают ввод или вывод и имеют когнитивную сложность <= 15
Жёлтые блоки - вывод, блоки, которые делают вывод и имеют когнитивную сложность <= 4
Оранжевые блоки - месиво, блоки которые и делают ио (их сложно тестить и сложно понимать из-за необходимости учитывать перформанс, обработку ошибок и порядок выполнения) и имеют когнитивную сложность > 4 (их сложно читать и хочется тестить)
Или вот ещё не раскрашенный пример:
И в идеале я стремлюсь (и очень часто у меня получается) к такому:
Но вы правы, что не всегда всё так просто и красиво. Однако достичь вот такого - вполне реально и в "волосатых" случаях:
Эта картинка как раз ближе к IOSP/IODA, чем к ФА. Ну и да, оранжевый блок я подчищу в понедельник - этот кусок только-только дописали просрав дедлайн из-за недооценки "волосатости" задачи:)
Разница в сложности задачи.
В вашем примере я не вижу сложной бизнес-логики, которую стоит выделять.
А если вы приведёте пример с действительно сложной в моём пониманиии бизнес-логикой - мне скорее всего будет а) лень б) не хватать вводных, чтобы отрефакторить его и разделить логику и ио:)
ПФА - это частный случай того, что вы называете многослойной архитектуры (как и чистая, и просто функциональная), который накладывает доп. ограничение в виде неизменямых данных.
Но! В моём окружающем мире люди называют многослойной архитектуру, в которой domain logic зависит от persistence layer (который может выглядеть интерфейсами Spring Data репозиториев). И от такой вариации многослойной архитектуры, ПФА отличается ещё и запретом на обращение к persistence layer из domain logic.
На самом деле, видимо, тут же кроется и ответ на ваш первый комментарий: если делать многослойную архитектуру так, как её делают вокруг меня - она ни от чего не спасает и проекты по ней быстро превращается в большой ком грязи. Тот же Project Daniel - там были даже отдельные Maven-модули application-services и domain-services. Но это не спасло проект от превращения в большой ком грязи.
А ваша многослойная архитектура с правилом "доменная логика, которая не связана с вводом-выводом" как таки и позволяет "уйти от жёсткости и хрупкости".
Добавьте туда модель данных в виде неизменямых классов и это ровно то, что я называю "Промышленной функциональной архитектурой".
Почему я считаю, что незименямость важна я писал тут. А реальный кейс (но без кода, к сожалению) как "конфетка превращается в шпинат" - здесь
Ещё, кстати, прикольная идея в этом ключе и без фокуса на разделении ио и логики - Integration Operation Separation Principle и Integration Operation Data API Architecture
Да ну как сказать. React (да и Compose на андроиде) именно так и делает, насколько я знаю, и не чё "пипл юзает".
Да, полагаю, под капотом они хачат реальную модель "на месте", но сами UI компоненты и их состояние (с Redux-ом по крайней мере) с точки зраения разработчика - чистые функции и неизменяемые данные. Опять же насколько знаю, сам ни на React, ни на Compose не писал ничего.
Но вообще я возьму самоотвод - UI в целом и его рендеринг в частности - не моя специализация.
Как я писал - в вашем конкретном примере я бы ничего не стал глобально менять. Там нет логики, на которую хотелось бы писать десятки тестов - ваш конкретный случай я бы вообще покрыл 4 интеграционными тестами - хеппи пас, и по тесту на каждую из ошибок.
Ещё, пожалуй, стоит проговорить, что цепочка - ввод -> бизнес-логика -> вывод - это идеал, которого часто, но не всегда можно достичь.
А вот штука, которой можно достичь практически всегда - это сендвич - io -> logic -> io -> logic -> io.
Но большой вопрос стоит ли оно того.
Плюс по моим канонам, сам io на внутри себя и на своём уровне абстракции может содержать логику. Вот тут у меня описан пример такого случая.
Вот ещё пара ссылок по теме, как упоровшись по ФП выжимать возможный максимум разделения ио и логики:
https://blog.ploeh.dk/2017/02/02/dependency-rejection/
https://blog.ploeh.dk/2019/12/02/refactoring-registration-flow-to-functional-architecture/
Не правильно.
Идеи тащтельного проектирования АПИ и разделения ио и логики - универсальные и годятся веде.
Код серверного рендеринга - естественно придётся выкинуть в трубу при разработке десктопного клиента.
Код бизнес-логики от вида клиента никак не зависит и будет работать и дальше.
Если вы захотите запустить весь сервис на дескотопе в одном процессе - выкинуть придётся код контроллеров (связывающий HTTP и бизнес-логику).
Если вы при этом захотите отказаться и от реляционки (хотя бы в виде SQLlite) в пользу файликов - так же придётся написать занового код репозиториев (связывающий бизнес-логику с постоянным хранилищем)
:)
Судя по этому сообщению, вы, путаете клиентский и серверный рендеринг:) Это такая технология древних, когда браузер отправляет HTTP-запросы, получает HTML и, действительно, ререндерит всю страницу. Вообще без JS-а.
Советую ознакомится с HTMX и идеями за этой либой - позволяет делать "good enough" UI за очень дёшево.