1 of 70

Функциональная архитектура и Spring Data JDBC

4 года в проде, полёт отличный

Включить запись!

2 of 70

Био

  1. Пишу код на Kotlin/Java с 2003 года
  2. С 2017 года в свободном плавании
  3. С 2020 года делаю коммерческие проекты на функциональной архитектуре, с января 2021 на Spring Data JDBC
    1. Делаю пятый проект на этой связке
    2. Самый большой проект - ~40К строк Kotlin-кода, 60 таблиц
    3. Все проекты не нагруженные - <10 rps, <10M строк

2

3 of 70

Агенда

  1. Большой ком грязи
  2. ФА спасёт отца русской демократии
  3. ФА + Spring Data JDBC = 💗
  4. Проблемы Spring Data JDBC

3

4 of 70

4

5 of 70

Коробочный продукт заказа VIP-такси

5

6 of 70

6

7 of 70

7

8 of 70

Большой ком грязи в вакууме

8

9 of 70

Проблема IO (ввода-вывода) №1 - он медленный

fun driverStats(driver: Driver): String = """

DriverStats(

name=${driver.name},

ordersInShift=${driver.ordersInShift})

"""

fun assignOrders() {

val onlineDrivers = //...

log.debug("Drivers stats after assign: {}",

onlineDriver.map(::driverStats))

}

9

10 of 70

Проблема IO (ввода-вывода) №1 - он медленный

fun driverStats(driver: Driver): String = """

DriverStats(

name=${driver.name},

ordersInShift=${driver.ordersInShift},

skills=${driver.skills})

"""

fun assignOrders() {

val onlineDrivers = //...

log.debug("Drivers stats after assign: {}",

onlineDriver.map(::driverStats))

}

10

11 of 70

Проблема IO №2 - он может не предсказуемо отказать

fun driverStats(driver: Driver): String = """

DriverStats(

name=${driver.name},

ordersInShift=${driver.ordersInShift},

skills=${skillsService.getSkills(driver.id)})

"""

fun assignOrders() {

val onlineDrivers = //...

log.debug("Drivers stats after assign: {}",

onlineDriver.map(::driverStats))

}

11

12 of 70

Проблема IO №3 - его сложно тестировать

class DriverStatsTests {

@Test

fun `Статистика водителя должна содержать его имя`() {

// Сетап

val name = "Гордеев Иван"

val driver = aDriver(name = name)

// Действие

val stats = Assigner.driverStats(driver)

// Проверка

stats shouldContain name

}

}

12

13 of 70

Проблема IO №3 - его сложно тестировать

@SpringBootTest

class DriverStatsTests {

@MockBean

private lateinit var skillsService: SkillsService

@Autowired

private lateinit var assignService: AssignService

@Test

fun `Статистика водителя должна содержать его имя`() {

val name = "Гордеев Иван"

val driver = aDriver(name = name)

every { skillService.getSkills(driver.id) }

returns listOf("Поддерживать разговор о политике")

val stats = AssignService.driverStats(driver)

stats shouldContain name

}

}

13

14 of 70

Проблема IO №4 - временнАя сцепленность

fun assignOrders() {

val onlineDrivers = //...

// ...

val assignments = findBestMatches()� tryAssign(assignments)

// ...

log.debug(

"Drivers stats after assign: {}",

onlineDriver.map(::driverStats))

}

14

15 of 70

Добавление io в метод существенно усложняет работу с ним

чтение и понимание, модификацию, переиспользование, тестирование.

15

16 of 70

16

17 of 70

17

18 of 70

18

19 of 70

Функциональная архитектура

19

20 of 70

Функциональная архитектура

// Модель

@Entity

class Order(

var id: Long

var price: Long

) {

fun applyDiscount() {

price = (price * 0.9).toLong()

}

}

// Репозиторий

interface OrdersRepo : CrudRepository</**/>

// Сервис приложения

class DiscountsService {

@Transactional

fun applyDiscount(orderId) {

Order order = ordersRepo

.findByIdOrNull(orderId)

?: throw NotFound()

order.applyDiscount()

}

}

20

21 of 70

Функциональная архитектура

// Модель

@Table("orders")

data class Order(

val id: Long

val price: Long

) {

fun applyDiscount(): Order =

copy(price = (price * 0.9).toLong())

}

// Репозиторий

interface OrdersRepo : CrudRepository</**/>

// Сервис приложения

class DiscountsService {

@Transactional

fun applyDiscount(orderId) {

Order order = ordersRepo

.findByIdOrNull(orderId)

?: throw NotFound()

val disctountedOrder =� order.applyDiscount()

ordersRepo.save(discountedOrder)

}

}

21

22 of 70

Функциональная архитектура в вакууме

22

23 of 70

Структура графов

23

Количество методов

76

43

Количество вызовов

95

42

24 of 70

Чистая когнитивная сложность

24

Максимальная сложность

26

17

Суммарная сложность

149

107

25 of 70

Когнитивная сложность с учётом IO

25

Максимальная сложность

52

17

Суммарная сложность

298

139

Средняя сложность

3.92

3.37

26 of 70

Когнитивная сложность с учётом IO

26

Максимальная сложность

52

17

Суммарная сложность

298

139

Средняя сложность

3.92

3.37

Причём тут ФА?

27 of 70

27

Этот код нарушает SRP!

Он делает две вещи!

Перепиши его!

SRP это не про вещи!

И в любом случае этот код делает одну вещь на более высоком уровне абстракции!

Ой, точно!�Спасибо, щяс поправлю

У тебя тут эффектик в чистое ядро пробрался

Чистый код, SOLID, DRY, KISS, YAGNI, GRASP и т.д.

Функциональная архитектура

28 of 70

Гайдлайн ФА

  1. Делите код на императивную оболочку и чистое ядро
  2. Исключите ввод-вывод и в целом мутации в чистом ядре
  3. Ограничьте когнитивную сложность императивной оболочки 4 единицами

28

29 of 70

Инновация 60-ых годов

I made up the term 'object-oriented', and I can tell you I didn't have C++ in mind

– Alan Kay, OOPSLA '97

It <OOP> can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them.

– Alan Kay, Dr. Alan Kay on the Meaning of “Object-Oriented Programming

29

30 of 70

Результаты применения ФА

  1. Решили задачу в сжатые сроки (2 недели на первую версию)
  2. Ускорили ключевую функцию системы в 300 раз
  3. Упростили ключевую функцию системы в 2-3 раза
  4. Создали задел для сохранения этой простаты функции

30

31 of 70

А причём тут Spring Data JDBC?

31

32 of 70

ФА + SDJ = 💗

32

33 of 70

ФА + SDJ = 💗

33

ФА требует неизменяемой модели данных (Java record, Kotlin data class)

SDJ рекомендует использование неизменяемых сущностей

ФА требует декомпозиции модели на независимые элементы

SDJ требует декомпозиции модели на агрегаты

ФА требует удаления циклов из модели

SDJ требует удаления циклов из модели

ФА требует разделения IO и логики

SDJ не делает никакого IO под ковром

ФА требует тщательного проектирования IO

SDJ требует тщательного проектирования IO

34 of 70

34

35 of 70

Как делаем наследование

Как-то. Пробовали через jdbcAggregateTemplate и VIEW UNION-ов и через JSONB. Это боль.

35

36 of 70

Как делаем загрузку нескольких агрегатов

36

37 of 70

Как делаем динамические запросы

37

38 of 70

Плюсы/минусы

38

Простой и понятный код всей системы

Немного больше кода в слое персистанса

39 of 70

Резюме

  1. ФА позволяет существенно упростить код
  2. ФА и SDJ созданы друг для друга
  3. С SDJ придётся писать больше кода руками, особенно SQL

39

40 of 70

Попробуйте связку ФА и SDJ

Ридлист по ФА�

Проект на ФА и SDJ

40

41 of 70

Бонус-трэк

41

42 of 70

А чё по перформансу?

  1. У меня были ненагруженные проекты с <10 rps и <10M строк
  2. Ни разу ничего оптимизировать не требовалось
  3. Точно знаю: загружать списки агрегатов со связями 1-N - будет проблема k*N+1, где k - кол-во связей 1-N в агрегате
    1. Потенциально когда-нибудь может быть решат: single query load
  4. Полагаю: если исключить использование SQL, уметь готовить Hibernate и задаться оптимизацией - Hibernate будет быстрее
  5. Полагаю: если не уметь готовить Hibernate или не думать о ленивой загрузке в сложных операциях SDJ будет быстрее на порядки

42

43 of 70

ФА + JPA = 🤕

43

ФА требует неизменяемой модели данных (Java record, Kotlin data class)

JPA не может работать с неизменяемой моделью

ФА требует декомпозиции модели на независимые элементы

JPA лучше работает со связной моделью

ФА требует удаления циклов из модели

JPA лучше работает с циклами в модели

ФА требует разделения IO и логики

JPA изо всех сил прячет IO под ковёр

ФА требует тщательного проектирования IO

JPA позволяет игнорировать IO до последнего

44 of 70

А чё делать с клиентскими ID-ами?

44

45 of 70

А чё делать повторной загрузкой сущности?

45

46 of 70

А чё делать с удалением всех вложенных сущностей при обновлении?

46

47 of 70

Как выполняли замеры?

47

48 of 70

Архив

48

49 of 70

Функциональная архитектура

49

Uncle Bob:

But, perhaps most importantly, this code was written to be 100% functional. No variables were mutated, anywhere in the code.

�Space Warhttps://blog.cleancoder.com/uncle-bob/2021/11/28/Spacewar.html

50 of 70

Получилось ли решить?

50

51 of 70

51

52 of 70

52

53 of 70

53

@Service�public class CoreEntityService {�� public CoreEntity createCoreEntity(� CreateCoreEntityRequest request, � AdditionalParams additionalParams) {�� CoreEntity coreEntity = createCoreEntity(request, additionalParams);� � boolean remindCreator = coreEntity.getType().getNotificationSettings().remindCreator();� if (!coreEntity.getType().isDisableFunctionalityForCreator() && remindCreator) {� notifyService.notify(� coreEntityMessageService.createOnNewCoreEntityForCreatorMessage(coreEntity, true)� );� }�� return coreEntity;� }��}

54 of 70

54

https://www.sonarsource.com/docs/CognitiveComplexity.pdf

55 of 70

55

56 of 70

56

57 of 70

57

58 of 70

58

59 of 70

59

  • 300+ методов Spring-бинов
  • 30+ репозиториев
  • 30+ find*
  • 20+ save
  • Один бог знает сколько ленивой �загрузки и сохранения через �дёрти чекинг

60 of 70

Какое решение придумали (за 2015-2020 гг.)

  1. Декомпозиция модели на агрегаты (по мотивам DDD)
  2. Функциональная архитектура

60

61 of 70

Агрегаты

61

62 of 70

Какую проблему решаем №2

JPA 2.1 слабо дружит с агрегатами и JPA любой версии вообще не дружит с функциональной архитектурой

62

63 of 70

JPA и агрегаты

В первую очередь в Hibernate 4 не было возможности делать запросы по незамапленным связям, в том числе обратные JOIN-ы для однонаправленных связей.

63

64 of 70

JPA и функциональная архитектура

An update to the state of an entity includes both the assignment of a new value to a persistent property or field of the entity as well as the modification of a mutable value of a persistent property or field

— JSR 338: JavaTM Persistence API; Version 2.2; "3.2.4 Synchronization to the Database"

64

65 of 70

JPA и функциональная архитектура

public class User {� @Id� @GeneratedValue(strategy = GenerationType.IDENTITY)� private final Long id;� private final String name;�}��public void test() {� var u = new User(null, "name");� var id = transactionTemplate.execute(status -> usersRepo.save(u).getId());� transactionTemplate.execute(status -> {� var user = usersRepo.findById(id).orElseThrow();� var updatedUser = new User(user.getId(), "updated");� usersRepo.save(updatedUser); // меняет “неизменяемый” объект user 😱return null;� });�}

65

66 of 70

Получилось ли решить?

66

67 of 70

Получилось ли решить?

67

68 of 70

Получилось ли решить?

68

69 of 70

Получилось ли решить?

69

Оригинальный проект

Реинжиниринг (v1)

Архитектура

Микросервисы + вертикальная

Монолит + функциональная

Реализация v1.1

Реализация v1.1

Реализация базовой версии

189 ч/дней мидла

145 ч/дней (~75%) юниора

Покрытие тестами

0 тестов

100% покрытие эндпоинтов

Доработки v1 -> v1.1

Доработки v1.1 -> v1.2

Медианные трудозатраты на задачу

16 часов

5 часов

Среднее количество багов на задачу

1.5 шт.

0.5 шт.

Подробности: https://azhidkov.pro/posts/23/07/project-e-results/

70 of 70

SDJ vs jooq, MyBatis, EBeans, Kotlin Exposed и т.д.

  1. Spring в названии - проще “продавать” клиентам/заказчикам/начальникам
  2. Во многом привычный DevX - проще “продавать” коллегам/команде
  3. Не надо писать простые запросы руками
  4. Есть автоматическое каскадное сохранение и загрузка агрегатов

70