Pull to refresh
42
0
Алексей @jdev

Автор Эргономичного подхода, Kotlin/Backend техлид

Send message

Последние 4 года 90% моих тестов такие и есть - интеграционные, с БД в тестконтейнере и запросами по ХТТП.

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


Два оснонвых секрета:
1) Не использовать @DynamicPropertySource, потому что это приводит к инвалидации контекста в кэше и запуску контекста для каждого тест-кейса
2) Использовать RAM-диск для постгреса.

Вместо DynamicPropertySource я использую такой трюк:

@ContextConfiguration(
    // ...
    initializers = [TestContainerDbContextInitializer::class]
)

class TestContainerDbContextInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {

    override fun initialize(applicationContext: ConfigurableApplicationContext) {
        // это небольшая функция-расширение, которая просто перетирает 
        applicationContext.overrideProperties(
            "spring.datasource.username" to pgContainer.username,
            // ...
        )
    }
}

А чтобы посадить Postgres на рам-диск - такой:

        .withTmpFs(mapOf("/var" to "rw"))
        .withEnv("PGDATA", "/var/lib/postgresql/data-no-mounted")

В результате, у меня на i7-8700, 32 RAM, SSD интеграционные тесты выполняются от 14мс при тесте с моками в 163 мс:

А в проекте со скрина, я пошёл ещё радикальнее - отказался от @SpringBootTest и запускаю приложание руками, а в локальной разработке сначала ищу предзапущенную БД:

val context: ConfigurableApplicationContext by lazy {
    SpringApplicationBuilder(TestsConfig::class.java)
        .profiles("test")
        .build()
        .run()
}

@Import(
    QYogaApp::class,
    BackgroundsConfig::class,
    TestPasswordEncoderConfig::class,
    TestDataSourceConfig::class,
    TestMinioConfig::class,
    FailingController::class
)
@Configuration
class TestsConfig

private const val DB_USER = "postgres"
private const val DB_PASSWORD = "password"

val jdbcUrl: String by lazy {
    try {
        val con = DriverManager.getConnection(
            PROVIDED_DB_URL.replace("qyoga", DB_USER),
            DB_USER,
            DB_PASSWORD
        )
        log.info("Provided db found, recreating it")
        con.prepareStatement(
            """
                DROP DATABASE IF EXISTS qyoga;
                CREATE DATABASE qyoga;
            """.trimIndent()
        )
            .execute()
        log.info("Provided db found, recreated")
        PROVIDED_DB_URL
    } catch (e: SQLException) {
        log.info("Provided Db not found: ${e.message}")
        pgContainer.jdbcUrl
    }
}

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

А есть какие-то исследования, подтверждающие, что JPA это стандарт?

Я сам ограничился этим :)

У обеих компаний десятки миллионов клиентов.

Мне кажется тут надо смотреть не на количество клиентов, а на количество компаний/разработчиков.

Но вообще я вполне допускаю, что JPA стандарт только в моём инфопузыре и объективно она не так популярна, как мне кажется.

А и самое главное забыл - SDJ интегрируется с MyBatis из коробки:)

То есть вы можете писать в БД через SDJ, со всеми его плюшками, а читать через MyBatis со всеми плюшками ещё и MyBatis.

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

Не совсем понял ваш посыл.

Константин, проводил свои исследования в 60-ых - лет за 40-50 до хайпа ФП.

При этом мы не наблюдаем взрыва популярности языков программирования
которые хоть как-то поддерживают ФП парадигму более менее достойно,
вроде Scala, F#. Мы наблюдаем как множество экспертов-практиков пишут все в тех же языках с императивной парадигмой вроде Java, C# и т.д.

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

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

Почему мы не наблюдаем большого количества больших и значимых проектов на Haskell?

Полагаю, основная причина - потому что в ВУЗах учат C/Java/Python etc.

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

Ненене, я в своём уме, я бы никогда не стал этого делать:) Этот пример не про то, что ФП быстрее, а про то, что ФП "понятнее" для компилятора, из чего я делаю предположение, что оно и для человека понятнее.

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

Ну тут мне кажется мы снова упираемся в вопрос, что такое ФП.

Например

// написано в браузере
fun calculateSalary(e: Employee): Int = 
  TODO() // тут чистая математика

fun main () {
    val e = db.getEmployee(readLine().toInt())
    val salary = calculateSalary(e)
    paymentGateway.pay(e, salary)
}

Для меня - ФП. Нужна ли для докторская для чтения этого кода? Нет

Нет. В качестве доказательств нужна статистика, сколько что и за сколько производится с описанием почему

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

Другое дело, что исходных данных нет - это да. Но так я и говорил о Гипотезе:)

Делать выводы что ФП ускоряет разработку на основании что эксперты что-то там в своих книгах пишут - не очень серьезный подход

Вы меня не верно поняли:) Я делаю вывод на основании эмпирического исследования Константина и собственного опыта. А эксперты и книги - это шло в разделе "Косвенные доказательства".

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

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

А вот что будет перед и после этого кода - зависит от задачи.

Как вариант, у вас на обеих сторонах может быть по координатору

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

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

и шо то черная магия, шо то черная магия

Я согласен, что SDJ - тоже чёрная магия. Но, для меня по крайней мере, несопоставимо более простая чем JPA. С SDJ, я могу работать практически не заглядывая в доки и гугл (после прочтения этой самой доки и сбора базовых грабель), а с JPA постоянно приходилось гуглить заклинания, которые надо скастовать для получения требуемого результата.

Плюс я сейчас в петпроекте эксперементирую с отказом от генерации репозов в пользу самописанных на базе jdbcAggregateTemplate - это ещё пласт магии уберёт

Закидываешь иммутабельный объект в метод, получаешь иммутабельный
результат из метода - всё, никаких сессий, состояний, актив рекордов и
т.п. Плюс удобно писать сколь угодно безумные запросы с CTE, оконками и любыми причудами

В SDJ всё точно так же.

Плюс SDJ обещает в будущих версиях "решить проблему N+1 раз и навсегда" - это выглядит любопытно для меня.

Мне MyBatis, jooq (+ Exposed) - тоже симпатичны. Но чего мне в них нехватает - возможности сохранить дерево объектов "автомагически".

Я ни в коем случае не утверждаю, что ФП-программы быстрее императивных программ (хорошо написанных).

Этот пример не про то, что ФП-программы быстрее, а про то, что компилятор лучше "понимает" ФП программы и я это использую как аргумент в пользу того, что ФП-программы "понятнее".

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

Могу предложить посмотреть на https://git.codemonsters.team/guides/ddd-code-toolkit/-/tree/dev - у Макса подход очень близок к моему, но у него структура функции-координатора на монадах.

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

Спасибо:)

Обсуждать и тыкать плюсы (и минусы) можно (и нужно) телеграмме:)

Понял, спасибо.


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

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

Угу, я так же извернулся.

По поводу Persistence, если я правильно понял вопрос

Кажется не правильно поняли:)

Вот эта функция:

async def get_users() -> list[User]:

Я так понял - это статическое определение именно функции, а не переменной функционального типа. Соответственно подключение к внешнеей системе (БД, например) она берёт из глобальной переменной.

Отсюда я вижу два следствия:

  1. При её использовании, надо догадаться что эту глобальную переменную надо про инициализировать

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

Или у вас всё-таки что-то в таком духе:

fun getUsers(ds: DataSource): List<User> = { ds.fetchUsers() }

val db1Users = getUsers(DataSource(db1))
val db2Users = getUsers(DataSource(db2))

class Client(private val getUsers: () -> List<User>) {
  fun showUsers() {
    getUsers().forEach { println(it) }
  }
}

fun main() {
  val client = Client(db1Users)
  client.showUsers()
}

Ну т.е. как вы инжектите инфраструктуру в такие функции?

Касательно второго, комментария, я утверждаю (самовнушаю ), что следующая запись является эквивалентом предыдущей:

class Users(private val ds: DataSource) {}
    fun getUsers(): List<User> = ds.fetchUsers()
}

class Client(private val users: Useres)) {
  fun showUsers() {
    users.getUsers().forEach { println(it) }
  }
}

fun main() {
  val client = Client(Users(DataSource(db1)))
  client.showUsers()
}

То есть конструктор выполняет функцию частичного применения своих параметров к методам объекта. Забыл уже терминологию

А если в рантайме собираете - чем это отличается от объекта?:) Я опять же отошёл от чистой функциональности и утешаю себя тем, что конструктор(p1, p2) + метод(p3) = функции(p1, p2, p3) :)

Фантастика. В хорошем смысле слова.

У меня есть пара практических вопросов.

Я от ROP-а в чисто функциональном стиле отказался как раз из-за синтаксического мусора, который он генеряет в Kotlin. Вы вроде сказали, что в Python те же проблемы, но не рассказали как это в итоге делаете. Расскажете?

Затем, функции в persistance. Они всё-таки по природе своей stateful (имеют источник подключения к БД условной). Вы их в итоге к глобальному окружению приколачиваете? Или через частичное применение в рантайме собираете? Или ещё как-то? Так же буду благодарен за подробности.

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

А за State-driven и action-driven - отдельное спасибо. Я пару лет думал на эту тему в фоне и ни как не мог это лаконично сформулировать.

Слово микросервисы в названии - ради кликбейта, у меня МСы имеют штраф в 100 у.е. и я начинаю бить систему на разные процессы только когда по другому никак:)

Что увидел в нотации по сравнению со Стормингом. События — События,
Ресурсы — Агрегаты, Эффекты — Команды, Операции — Группы команд

Сходство и правда есть, но не полное.
Ресурсы - включают агрегаты, но не только.
Эффекты - это отдельные операции по чтению и модификации состояния
Операции - вот это аналог команд.

Отказываемся от пользователя, экспертов домена, их глоссария.

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

теряем часть их NFRов

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

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

К текущему моменту этот подход я апробировал на 6 коммерческих проектах 1-12 человеко-месяцев. И я не думаю, что я буду когда-то "up front" проектировать систему на 10 человеко-лет. А если система не помещается в одну голову - это уже 101 балл в пользу разбиения системы на несколько.

подмножество Event Storming

Это не совсем так.

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

упрощенная до трехзвенной модель.

Это совсем не так:)

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

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

Ну и мне всё больше кажется, что вы говорите о "проблеме выражения". Плюс вспомнил ООПный способ её решения

За книжку - спасибо, добавил себе в список прочтения.

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

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

Я не могу понять "требования" за этим кодом. Все 4 операции доступны для конечного пользователя? Операции записи и печати как-то связанны между собой? Например печать берёт отчёт, ранее записанный в базу? Откуда берётся отчёт для записи в базу?

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

Information

Rating
5,041-st
Location
Кольцово, Новосибирская обл., Россия
Date of birth
Registered
Activity

Specialization

Chief Technology Officer (CTO), Software Architect
Lead
From 350,000 ₽
Functional programming
Object-oriented design
Design information systems
TDD/BDD
Kotlin
PostgreSQL
Java Spring Framework
Linux
Git
Docker