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
Does not participate
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