Функциональная архитектура и Spring Data JDBC
4 года в проде, полёт отличный
Включить запись!
Био
2
Агенда
3
4
Коробочный продукт заказа VIP-такси
5
6
7
Большой ком грязи в вакууме
8
Проблема 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
Проблема 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
Проблема 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
Проблема IO №3 - его сложно тестировать
class DriverStatsTests {
@Test
fun `Статистика водителя должна содержать его имя`() {
// Сетап
val name = "Гордеев Иван"
val driver = aDriver(name = name)
// Действие
val stats = Assigner.driverStats(driver)
// Проверка
stats shouldContain name
}
}
12
Проблема 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
Проблема IO №4 - временнАя сцепленность
fun assignOrders() {
val onlineDrivers = //...
// ...
val assignments = findBestMatches()� tryAssign(assignments)
// ...
log.debug(
"Drivers stats after assign: {}",
onlineDriver.map(::driverStats))
}
14
Добавление io в метод существенно усложняет работу с ним
чтение и понимание, модификацию, переиспользование, тестирование.
15
16
17
18
Функциональная архитектура
19
Функциональная архитектура
// Модель
@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
Функциональная архитектура
// Модель
@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
Структура графов
23
Количество методов | 76 | 43 |
Количество вызовов | 95 | 42 |
Чистая когнитивная сложность
24
Максимальная сложность | 26 | 17 |
Суммарная сложность | 149 | 107 |
Когнитивная сложность с учётом IO
25
Максимальная сложность | 52 | 17 |
Суммарная сложность | 298 | 139 |
Средняя сложность | 3.92 | 3.37 |
Когнитивная сложность с учётом IO
26
Максимальная сложность | 52 | 17 |
Суммарная сложность | 298 | 139 |
Средняя сложность | 3.92 | 3.37 |
Причём тут ФА?
27
Этот код нарушает SRP!
Он делает две вещи!
Перепиши его!
SRP это не про вещи!
И в любом случае этот код делает одну вещь на более высоком уровне абстракции!
Ой, точно!�Спасибо, щяс поправлю
У тебя тут эффектик в чистое ядро пробрался
Чистый код, SOLID, DRY, KISS, YAGNI, GRASP и т.д.
Функциональная архитектура
Гайдлайн ФА
28
Инновация 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
А причём тут Spring Data JDBC?
31
ФА + SDJ = 💗
32
ФА + SDJ = 💗
33
ФА требует неизменяемой модели данных (Java record, Kotlin data class) | SDJ рекомендует использование неизменяемых сущностей |
ФА требует декомпозиции модели на независимые элементы | SDJ требует декомпозиции модели на агрегаты |
ФА требует удаления циклов из модели | SDJ требует удаления циклов из модели |
ФА требует разделения IO и логики | SDJ не делает никакого IO под ковром |
ФА требует тщательного проектирования IO | SDJ требует тщательного проектирования IO |
34
Как делаем наследование
Как-то. Пробовали через jdbcAggregateTemplate и VIEW UNION-ов и через JSONB. Это боль.
35
Как делаем загрузку нескольких агрегатов
36
Как делаем динамические запросы
37
Плюсы/минусы
38
Простой и понятный код всей системы
Немного больше кода в слое персистанса
Резюме
39
Попробуйте связку ФА и SDJ
Ридлист по ФА�
Проект на ФА и SDJ
40
Бонус-трэк
41
А чё по перформансу?
42
ФА + JPA = 🤕
43
ФА требует неизменяемой модели данных (Java record, Kotlin data class) | JPA не может работать с неизменяемой моделью |
ФА требует декомпозиции модели на независимые элементы | JPA лучше работает со связной моделью |
ФА требует удаления циклов из модели | JPA лучше работает с циклами в модели |
ФА требует разделения IO и логики | JPA изо всех сил прячет IO под ковёр |
ФА требует тщательного проектирования IO | JPA позволяет игнорировать IO до последнего |
А чё делать с клиентскими ID-ами?
44
А чё делать повторной загрузкой сущности?
45
А чё делать с удалением всех вложенных сущностей при обновлении?
46
Как выполняли замеры?
47
Архив
48
Функциональная архитектура
49
Uncle Bob:
But, perhaps most importantly, this code was written to be 100% functional. No variables were mutated, anywhere in the code.
�Space War�https://blog.cleancoder.com/uncle-bob/2021/11/28/Spacewar.html
Получилось ли решить?
50
51
52
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
https://www.sonarsource.com/docs/CognitiveComplexity.pdf
55
56
57
58
59
Какое решение придумали (за 2015-2020 гг.)
60
Агрегаты
61
Какую проблему решаем №2
JPA 2.1 слабо дружит с агрегатами и JPA любой версии вообще не дружит с функциональной архитектурой
62
JPA и агрегаты
В первую очередь в Hibernate 4 не было возможности делать запросы по незамапленным связям, в том числе обратные JOIN-ы для однонаправленных связей.
63
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
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
Получилось ли решить?
67
Получилось ли решить?
68
Получилось ли решить?
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/
SDJ vs jooq, MyBatis, EBeans, Kotlin Exposed и т.д.
70