Pull to refresh
60
20
Алексей @jdev

Эксперт по эффективной разработке, Kotlin техлид

Send message

Очень хорошая статья.

Спасибо:)

ак с точки зрения проверки типов работает, что вы в мапу списков помещаете не список, а единственное значение? Я вижу, что groupedRow: Map<String, List<Any?>> , но не понимаю, что такое делает + , что тип стал Map<String, Any>

List<Any?> - является подтипом Any.

"оператор" + - это на самом деле просто синтаксический сахар для создания копии мапы и добавления в неё элемента (копирование вместо добавления в имеющуюся - из-за ФП головного мозга)

Соответственно:

  1. Метод groupBy возвращает Table aka List<Map<String, Any>>

  2. Лямбда, которая передаётся в rowGroups.map возвращает Map<String, List<Any?>>

  3. А сам метод map возвращает List<Map<String, List<Any?>>>

  4. И при возврате из метода groupBy выполняется upcast (приведение к супертипу) List<Map<String, List<Any?>>> -> List<Map<String, Any>>

@razon

И как вы находите границы микросервисов и ограниченных контекстов?

Условно ко мне пришёл продакт, говорит хочу фичу логистики, она должна делать то-то и то-то - в результате какого процесса у меня появятся артефакты/термины "ПВЗ Customer", "ПВЗ Маппер" и т.д. и "Ограниченный контекст ПВЗ", "Ограниченный контекст Геоданные" и т.д.

@razon

Поясните, пожалуйста - куда и на каком уровне абстракции фронт встраивается в вашей модели?

В моей практике фронтовые приложения/компоненты зачастую работают со множеством (всеми) слабосвязанных между собой компонентов бэка и, кажется, сломают всю стройную картину - если фронт засунуть в один из ограниченных контекстов - он его coupling убьёт, а если в отдельный - у него самого ещё хуже coupling будет.

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

Я пишу, что отношение тестов без моков к тестам с моками может быть 100 к 1. То есть без моков никак - бывает в одном случае из ста. В моей практике последних 4 лет - 9 коммерческих проектов суммарно на ~100К строк Котлин-кода.

При этом среднее время теста по моим проектам - порядка 50мс:

Скрин тестов публичного Trainer Advisor-а
Скрин тестов публичного Trainer Advisor

Тут ещё, пожалуй, стоит пояснить, что под "моками" я понимаю именно мокирование собственных типов с помощью условного Mockito. А против условных WireMock, GreenMail и т.п. я ничего не имею, включая верификацию вызовов - это часть наблюдаемого (снаружи) повдениия системы, которая должна быть специфицирована в тестах.

Добавлю ещё, что эти тезисы подтверждаются фактами из моей практики

В моём предыдущем проекте с тестами без моков за 500 человеко-часов, которые я рассматривал в ретроспективе было 0.5 бага на задачу или 0.05 бага/человеко-час (подробности).

В текущем проекте за примерно человеко/год разработки нашли 2 бага (не соответствие поведения софта требованиям) и 0 регрессий. Проект ещё не завершён, поэтому ретру не проводил и точных цифр пока нет.

В этом же проекте с 93% покрытием кода тестами без моков я недавно сделал пару серьёзных изменений в модели и рефакторингов: сделал один из агрегатов частью другого, ввёл разделение доменной модели и модели персистанса, добавил версионирование частей агрегата (условно таблички и её строк) - в ходе этой работы не было внесено ни одной регрессии.

А рефакторинг (20 затронутых файлов файлов, 482 добавленных строки, 172 удалённых строки) не содержал ни одной строки изменений в тестах.
Это красноречивая иллюстрация того, что хорошие тесты проверяют поведение системы, а не её реализацию и, как следствие, не меняются при рефакторинге - изменении структуры кода, без изменения его поведения.

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

Оставлю здесь мудрость древних для будущих поколений:

My personal practices…​ I’m mock almost nothing. If I can’t figure out how to test efficiently with the real stuff I find another way of creating a feedback loop for myself…​ I just don’t go very far down the mock path. I look at a code where you have mocks returning mocks returning mocks and…​ my experience is if I have…​ If I use TDD I can refactor stuff and then I heard these stories people say well I use TDD and now I can’t refactor anything and I feel like I couldn’t understand that and then I started looking at their tests…​ Well if you have mocks returning mocks returning mocks your test is completely coupled to the implementation…​ not on the interface but the exact implementation of some object …​ three streets away …​ of course you can’t change anything without breaking a test. So that for me is too high a price to pay that’s not not a trade-off I’m willing to make.

---

Мои личные практики…​ Я почти ничего не мокаю. Если я не могу понять, как эффективно тестировать c реальными зависимостями, я нахожу другой способ создать для себя цикл обратной связи…​ Я просто не хожу далеко по пути мокирования. Я смотрю на код, в котором у вас есть моки, возвращающие моки, возвращающие моки, и…​ мой опыт показывает, что если у меня есть…​ Если я использую TDD, я могу рефакторить кода, а потом я слышу эти истории, когда люди говорили, что я использую TDD, а теперь я ничего не могу отрефакторить, и я не мог этого понять, пока не начал начал смотреть на их тесты…​ Ну, если у вас моки возвращают моки возвращающие моки, то ваш тест полностью связан с реализацией…​ не с интерфейсом, а с точной реализацией какого-либо объекта…​ в трех кварталах отсюда…​ конечно, вы ничего не сможете изменить, не сломав тест. Так что для меня это слишком высокая цена, на которую я не готов пойти ни в коем случае.

— Кент Бек, основатель движения TDD, автор JUnit
TW Hangouts | Is TDD dead?

Personally I’ve always been a old fashioned classic TDDer and thus far I don’t see any reason to change. I don’t see any compelling benefits for mockist TDD, and am concerned about the consequences of coupling tests to implementation.

This has particularly struck me when I’ve observed a mockist programmer. I really like the fact that while writing the test you focus on the result of the behavior, not how it’s done. A mockist is constantly thinking about how the SUT is going to be implemented in order to write the expectations. This feels really unnatural to me.

---

Лично я всегда был сторонником старомодного классического подхода и до сих пор не вижу причин меняться. Я не вижу никаких убедительных преимуществ для mockist TDD и обеспокоен последствиями привязки тестов к реализации.

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

— Мартин Фаулер, автор таких книг как "Рефакторинг" и "Шаблоны корпоративных приложений"
Mocks Aren’t Stubs

In short, however, I recommend that you mock sparingly. Find a way to test – design a way to test – your code so that it doesn’t require a mock. Reserve mocking for architecturally significant boundaries; and then be ruthless about it. Those are the important boundaries of your system and they need to be managed, not just for tests, but for everything.

And write your own mocks. Try to depend on mocking tools as little as possible. Or, if you decide to use a mocking tool, use it with a very light touch.

If you follow these heuristics you’ll find that your test suites run faster, are less fragile, have higher coverage, and that the designs of your applications are better.

---

Вкратце, я рекомендую вам использовать моки с осторожностью. Найдите способ тестирования – спроектируйте способ тестирования вашего кода, чтобы он не требовал моков. Оставьте моки для архитектурно значимых границ, и там безжалостно мокайте всё (дословный перевод: "и затем будьте безжалостны к нему"). Это важные границы вашей системы, и ими нужно управлять не только для тестов, но и для всего остального.

И пишите свои собственные моки. Старайтесь как можно меньше зависеть от инструментов мокирования. Или, если вы решите использовать инструмент для создания макета, используйте его минимально (дословный перевод: "очень лёгкими касаниями").

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

— Роберт Мартин, автор SOLID и Чистой архитектуры
When To Mock

Я пожалуй даже код левого блока оркистрации скину:)

private fun updateTable(updateTableRq: UpdateTableRq): Table? =
        transactionTemplate.execute {
            if (updateTableRq.correctionData.isNullOrEmpty()) {
                return@execute null
            }

            val correctedRows = updateTableRq.correctedRows()
            val rowsExternalIds = correctedRows.map { it.externalId }.toSet()
            val table = tablesService.findTableByExternalIds(rowsExternalIds)

            if (table == null) {
                return@execute null
            }

            if (correctedRows.size != table.rows.size) {
                throw TableRowNotFoundException()
            }

            val settings =  tablesSettingsRepo.findByDocumentId(table.outgoingDocument.id)
                                      ?: throw ResourceNotFoundException()

            val referenceExternalIds = correctedRows.collectReferenceFieldsValues(settings.fields)
            val referenceIds = referencesService.findReferenceMapByExternalIds(referenceExternalIds)

            val systemName = table.rows.first().externalId.systemName
            val parsedCorrections = correctedRows.associateBy(UpdatedRowRq::externalId) {
                it.parseFieldValues(systemName, settings.apiFieldsById, referenceIds)
            }

            tablesService.updateTable(table) {
                val rowCorrections = parsedCorrections[it.externalId.externalId]
                tableRowsFactory.updateRow(settings, it, rowCorrections)
            }
        }

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

Ещё так попробую проиллюстрировать свою точку зрения.

Разделением ИО и логики я борюсь с этим:

Легенда:

  • Синие блоки - оркестрация, блоки которые взывают более одного типа других блоков и имеют когнитивную сложность <= 4

  • Красные блоки - ввод, блоки которые делают ввод и имеют когнитивную сложность <= 4

  • Зелёные блоки - бизнес-логика, блоки, которые не вызывают ввод или вывод и имеют когнитивную сложность <= 15

  • Жёлтые блоки - вывод, блоки, которые делают вывод и имеют когнитивную сложность <= 4

  • Оранжевые блоки - месиво, блоки которые и делают ио (их сложно тестить и сложно понимать из-за необходимости учитывать перформанс, обработку ошибок и порядок выполнения) и имеют когнитивную сложность > 4 (их сложно читать и хочется тестить)

Или вот ещё не раскрашенный пример:

И в идеале я стремлюсь (и очень часто у меня получается) к такому:

Но вы правы, что не всегда всё так просто и красиво. Однако достичь вот такого - вполне реально и в "волосатых" случаях:

Эта картинка как раз ближе к IOSP/IODA, чем к ФА. Ну и да, оранжевый блок я подчищу в понедельник - этот кусок только-только дописали просрав дедлайн из-за недооценки "волосатости" задачи:)

Разница в сложности задачи.

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

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

ПФА - это частный случай того, что вы называете многослойной архитектуры (как и чистая, и просто функциональная), который накладывает доп. ограничение в виде неизменямых данных.

Но! В моём окружающем мире люди называют многослойной архитектуру, в которой domain logic зависит от persistence layer (который может выглядеть интерфейсами Spring Data репозиториев). И от такой вариации многослойной архитектуры, ПФА отличается ещё и запретом на обращение к persistence layer из domain logic.

На самом деле, видимо, тут же кроется и ответ на ваш первый комментарий: если делать многослойную архитектуру так, как её делают вокруг меня - она ни от чего не спасает и проекты по ней быстро превращается в большой ком грязи. Тот же Project Daniel - там были даже отдельные Maven-модули application-services и domain-services. Но это не спасло проект от превращения в большой ком грязи.

А ваша многослойная архитектура с правилом "доменная логика, которая не связана с вводом-выводом" как таки и позволяет "уйти от жёсткости и хрупкости".

Верхний уровень логики (application logic) занимается оркестрацией. Он обращается как к доменной логике, которая не связана с вводом-выводом, так и к другой группе функционала, которая связана только с вводом-выводом.

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

Почему я считаю, что незименямость важна я писал тут. А реальный кейс (но без кода, к сожалению) как "конфетка превращается в шпинат" - здесь

Ещё, кстати, прикольная идея в этом ключе и без фокуса на разделении ио и логики - 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) в пользу файликов - так же придётся написать занового код репозиториев (связывающий бизнес-логику с постоянным хранилищем)

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

@nin-jin, комментарий к этому посту

:)

Там на каждый чих происходит пересоздание всего мира модели страницы.

Судя по этому сообщению, вы, путаете клиентский и серверный рендеринг:) Это такая технология древних, когда браузер отправляет HTTP-запросы, получает HTML и, действительно, ререндерит всю страницу. Вообще без JS-а.

Советую ознакомится с HTMX и идеями за этой либой - позволяет делать "good enough" UI за очень дёшево.

Нет уж извольте нормальный вариант предоставить. А я посмотрю как Вы это сделаете со своим подходом.

Технически (в нормальных языках :troll:) это можно сделать так:

val a by lazy { aRepo.findByIdOrNull(id) ?: throw NotFoundException() }
// ...

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

PS>

Ну а если у вас Прям Нормальный Язык для ФА (Haskell) - оно всё автоматом будет лениво тянуться

Приведите пожалуйста пример как...

Чуть позже

На самом деле это довольно сложно в имеющихся условиях.

Во-первых, структура графа вызовов - это только один из трёх аспектов архитектуры, по которой я сейчас работаю.

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

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

В-третьих, в вашем примере практически нет бизнес-логики.

Но тем не менее, давайте я попробую.

Что бы я точно сделал:

  1. Собрал вычисление h в отдельный метод

  2. Сделал бы данные неизменяемыми, а сохранение обновлений явным

Возможно ещё обернул бы логические выражения проверок в методы с вменяемыми именами, особенно для b и c:

    public long fu(long id) {
        var a = aRepo.findById(id).orElseThrow();
        if(a.type() == PRIME) {
            throw new FuException();
        }
        var b = bRepo.findById(a.bId()).orElseThrow();
        if(b.isFour()) {
            throw new FuException();
        }
        var c = cService.compute(a, b);
        if(c.isDirect()){
            systemXClient.send(c);
        }
        var h = h(a, b,c);
        if (hClient(h).getStatus() == PROBLEM){
            throw new FuException();
        }
        var counterItem = hRepo.findCounterItem(h.id()).orElseThrow();
        var readyH = h.withStatus(READY);
        var readyCounter = counterItem.withStatus(READY);
        hRepo.save(readyH);
        hRepo.save(readyCounter);
        return counterItem.counterId();
    }

    public H h(A a, B b, C c) {
        var g = (a.price() + b.fine()) / c.cf();
        var f = mClient(a.id);
        return g * f;
    }

Но это всё косметика. Тут надо либо сильно глубже смотреть, либо в этом примере и правда бизнес-логика предельно простая и самоочевидная.

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

  1. Пример кода оркестрации и графа вызовов операции для операции со сложной бизнес-логикой

  2. Пример построения сложной модели представления

    1. Тривиальная оркестрация

    2. Развесистая логика на чистых функциях

  3. Пример построения docx-дока

    1. Тривиальная оркестрация

    2. Относительно развесистая логика построения в виде чистой функции

      1. Тут код - прям наскоряк написанный говнокод - это был прототип фичи, которая оказалось не востребованной, поэтому рефакторить я его не стал. Но это чистый говнокод - если ему в fetchImage подсунуть Map::get - он для одних и тех же аргументов всегда будет возвращать один и тот же результат.

то где настоящие боевые примеры?

Здесь, здесь, здесь и здесь

дайте мне удобный инструмент

Вот он

Приведите пожалуйста

Чуть позже

Ответ на ваши опасения относительно Функциональной (а Симан в дополнение к Фрактальной топит и за Функциональную) архитектуру так же есть в посте:

Information

Rating
520-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