1. Архітектурний рефакторинг — усе ще рефакторинг
Правило тут не змінюється, як би солідно не звучало слово «архітектурний»: навіть архітектурний рефакторинг залишається рефакторингом, поки ви правите внутрішню будову й не чіпаєте спостережувану ззовні поведінку. Усе питання — в одному: що побачить зовнішній споживач? А це не лише користувач у браузері. Це REST-клієнт, сусідній модуль, підписник на подію, тест, який перевіряє публічний контракт, інший сервіс, якому надходить ваше повідомлення. Їхня поведінка незмінна — ви на території рефакторингу.
Якщо перенести це на Commerce OS, картина стає наочнішою: ви можете винести публікацію події з OrderService в окремий клас, розбити довгий метод, провести межу між бізнес-логікою та інфраструктурою. Але змінили JSON-відповідь контролера, формат події, тему Kafka, залежність у build.gradle або бізнес-правило знижки — вийшли за межі задачі.
Найнаочніше це видно в такій таблиці:
| Зміна | Це рефакторинг? | Чому |
|---|---|---|
| Винести публікацію події в OrderEventPublisher | Так | Зовнішній контракт той самий, змінюється лише структура |
| Перейменувати processOrderV2 на finalizeOrder всередині модуля | Так | Імʼя стало зрозумілішим, поведінка не змінилася |
| Додати поле в REST-відповідь /api/orders/{id} | Ні | Змінюється зовнішній контракт API |
| Оновити spring-kafka або spring-boot | Ні | Це вже зміна залежностей, не рефакторинг |
| Виправити неправильну умову знижки | Ні | Це bugfix, тобто поведінка змінюється |
| Перейменувати topic orders.finalized на orders.done | Ні | Зовнішні підписники побачать іншу поведінку |
Якщо сказати зовсім по-людськи, архітектурний рефакторинг — це не «зробити проєкт сучасним», а «зробити одну межу чистішою, не чіпаючи домовленості із зовнішнім світом». Менш героїчно. Зате безпечніше.
Архітектурний рефакторинг залишається рефакторингом рівно доти, доки зовнішня спостережувана поведінка не змінилася.
2. Commerce OS: OrderService знає занадто багато
Найпростіший спосіб відчути архітектурний рефакторинг — не читати визначення, а подивитися на клас, який «ніби працює», але вже трохи підозріло схожий на співробітника, що виконує обовʼязки пʼяти відділів одразу. У Commerce OS такий кандидат — OrderService: він і завершує замовлення, і сам думає про публікацію події. Зовнішній flow не чіпаємо — очищуємо межу відповідальності.
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public void completeOrderFinalization(Order order) {
order.markFinalized();
orderRepository.save(order);
kafkaTemplate.send("orders.finalized", new OrderFinalizedEvent(order.getId()));
// Бізнес-логіка та публікація події змішані в одному місці
}
}
На перший погляд усе навіть пристойно: замовлення завершили, зберегли, подію відправили. Але проблема тут не в тому, що код не працює, а в тому, що OrderService одночасно знає про стан замовлення, про збереження в репозиторії та про механізм доставки події назовні. Клас тримає в голові бізнес-правило, репозиторій, Kafka і, здається, ще розклад електричок — сигнал звузити його кругозір.
Подивімося на чистішу межу. Спочатку виносимо публікацію події в окремий клас:
import org.springframework.stereotype.Component;
@Component
public class OrderEventPublisher {
public void orderFinalized(Order order) {
kafkaTemplate.send("orders.finalized", new OrderFinalizedEvent(order.getId()));
// Топік і payload залишилися тими самими
}
}
Тепер OrderService простіший:
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public void completeOrderFinalization(Order order) {
order.markFinalized();
orderRepository.save(order);
orderEventPublisher.orderFinalized(order); // Поведінка та сама, межа чистіша
}
}
Ззовні нічого не змінилося: той самий виклик completeOrderFinalization(order), той самий стан замовлення в базі, та сама подія в той самий topic. Але всередині зрозуміліше — OrderService координує бізнес-дію, OrderEventPublisher відповідає за інфраструктурний побічний ефект.
Цю саму думку зручно побачити схемою:
flowchart LR A[OrderController] --> B[OrderService] B --> C[OrderRepository] B --> D[OrderEventPublisher] D --> E[KafkaTemplate]
Саме так і виглядає хороший архітектурний рефакторинг. Без апокаліпсису, без «нової платформи», без бажання оновити півпроєкту. Ви звужуєте відповідальність кожного елемента — щоб наступна людина не читала бізнес-сервіс із думкою: «А чому він знає про Kafka?»
3. Напрямок залежностей без містики
Фраза «напрямок залежностей» часто звучить для новачка надто академічно, ніби зараз потрібно відкрити три книжки з архітектури й одну чашку валерʼянки. Магії немає — це відповідь на одне запитання: хто про кого знає. Чим менше бізнес-логіка знає про інфраструктуру, тим простіше код читати, тестувати й безпечно змінювати.
У нашому прикладі OrderService уже перестав напряму смикати kafkaTemplate, і це добре. Але можна зробити ще один акуратний крок: залежати не від низькорівневої деталі, а від зрозумілої межі. Іноді вистачає конкретного класу OrderEventPublisher, іноді корисний інтерфейс — відокремити бізнес-рівень від інфраструктури явно.
public interface OrderEvents {
void orderFinalized(Order order);
}
А publisher реалізує цей інтерфейс:
import org.springframework.stereotype.Component;
@Component
public class OrderEventPublisher implements OrderEvents {
@Override
public void orderFinalized(Order order) {
kafkaTemplate.send("orders.finalized", new OrderFinalizedEvent(order.getId()));
}
}
Що це дає на практиці? OrderService легше тестувати — реальний Kafka-клієнт йому більше не потрібен навіть подумки. І код краще виражає намір: сервіс не «працює з Kafka», а «повідомляє, що замовлення завершено». Різниця здається словесною, але саме такі словесні межі потім рятують великі класи від каші. Тест після цього чесніший:
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
@Test
void shouldDelegateEventPublication() {
service.completeOrderFinalization(order);
verify(orderEvents).orderFinalized(order); // Перевіряємо межу, а не транспорт
}
Зверніть увагу: safety net із попередніх лекцій ми не викинули — навпаки, тут він особливо потрібен. Unit-тест вище перевіряє, що сервіс делегує публікацію. Існуючий integration test підтверджує, що зовнішній flow не зламався: endpoint працює, подія надходить, контракт збережено. Так і поєднуються lightweight characterization, harness та архітектурний рефакторинг — кожен перевіряє свою межу.
І ще одна важлива деталь для тих, хто лише вчиться: не потрібно героїчно додавати інтерфейс усюди, де миготить слово «залежність». Інтерфейс — це інструмент, а не релігія: вводимо його там, де він робить межу зрозумілішою, а не заради відчуття архітектурної духовності.
4. Червона лінія: рефакторинг чи вже інша задача
Найнебезпечніший момент рефакторингу настає не в коді, а в голові. Ви винесли OrderEventPublisher, усе гладко, і тут десь збоку виникає спокуса: «Раз я вже тут, давайте заодно оновлю Kafka, перейменую topic, виправлю дивний if і почищу сусідній пакет». Ось у цей момент рефакторинг і перетворюється на підозрілий комбайн із трьох задач.
Щоб цього не сталося, корисно ставити собі дуже приземлене запитання: якщо reviewer побачить це в PR, він пояснить одним реченням, що відбулося? Якщо відповідь звучить як «тут і рефакторинг, і fix, і трохи інфраструктури» — час розділяти роботу.
Ось зручна таблиця для таких прикордонних випадків:
| Що вам захотілося зробити «заодно» | Що це насправді | Що робити правильно |
|---|---|---|
| Поміняти topic події або її payload | Зміна зовнішнього контракту | Окрема задача, не рефакторинг |
| Підняти версію spring-kafka | Оновлення залежності | Окремий PR |
| Виправити помилку в умові знижки | Bugfix | Окремий PR із regression check |
| Перейменувати поля в REST DTO | Зміна API | Окрема задача |
| Винести side effect в окремий collaborator | Рефакторинг | Можна робити зараз |
| Прибрати кілька сторонніх попереджень у сусідньому модулі | Супутнє прибирання по сусідству | Не робити в цьому PR |
Особливо підступна історія — «прихована міграція». Виглядає вона безневинно: ніби почистили архітектуру, але заодно змінили конфіг, сигнатуру події, формат серіалізації або поведінку залежності. Ззовні PR називається рефакторингом, а фактично поведінка системи вже змінилася. Це ламає головну обіцянку роботи: збережена поведінка.
Тому в архітектурного рефакторингу є дуже корисна дисципліна: сплив справжній bug, dependency upgrade або зміна контракту — зупиняєте поточний PR і відкриваєте окрему задачу. Не бюрократія — спосіб тримати diff зрозумілим, rollback дешевим, review чесним. Інакше отримаєте красиву назву PR і некрасивий ризик усередині.
5. Критерії приймання для PR з рефакторингу
Поки рефакторинг живе лише у вашій голові, він здається зрозумілим майже завжди. Справжня перевірка починається в той момент, коли PR відкриває інша людина — або reviewer-agent із Workflow Kit. Тому критерії приймання тут мають бути не абстрактними, а цілком приземленими: що залишилося тим самим, що стало кращим, чим це підтверджено.
Для архітектурного PR з рефакторингу в Commerce OS зручно використовувати таку таблицю:
| Критерій | Чим його доводимо |
|---|---|
| Зовнішня поведінка не змінилася | Integration/API checks зелені, топік і payload події ті самі, публічні сигнатури не змінювалися |
| Тести й локальні перевірки зелені | Harness із CLAUDE.md: unit, integration, build, lint, type-check, якщо вони застосовні |
| Diff зрозумілий за один прохід | PR тримається навколо однієї ідеї та обмеженого набору файлів |
| Складність стала нижчою | Метод коротший, менше розгалужень, відповідальність уже |
| Дублювання стало меншим | Повторюваний код винесено в окремий collaborator |
| Імена стали зрозумілішими | Нові назви пояснюють намір, а не ховають його |
| Випадкових змін залежностей немає | build.gradle, package.json, конфіги залежностей не чіпали |
| Прихованої міграції немає | Не змінювалися версії, транспортні налаштування, зовнішній формат подій та API |
| Немає широкого стороннього cleanup | У diff немає лавини стороннього форматування та випадкових перейменувань по сусідству |
| Відкат можливий дешево | PR можна відкотити одним revert без каскаду побічних дій |
Зверніть увагу: критерії приймання перевіряють не лише безпеку, а й користь. Якщо diff величезний, а відповідальність не стала зрозумілішою, reviewer цілком має право не радіти. PR з рефакторингу відповідає на два запитання разом: чи нічого ми не зламали і чи справді стало легше жити з цим кодом далі.
Хороший PR description для нашого прикладу може виглядати так:
## Мета
Ізолювати публікацію подій із `OrderService`, не змінюючи зовнішній контракт сервісу.
## Обсяг змін
`OrderService`
`OrderEventPublisher`
`OrderServiceTest`
## Що не змінювалося
REST `/api/orders/{id}/finalize`
topic `orders.finalized`
payload `OrderFinalizedEvent`
залежності проєкту
## Перевірки
`./gradlew test --tests '*OrdersUnitTest'`
`./gradlew test --tests '*OrdersIntegrationTest'`
А reviewer.md із Workflow Kit може повернути, наприклад, такий короткий підсумок у REVIEW_NOTES.md:
Статус: BEHAVIOR_PRESERVED
Публічні символи: без змін
Перевірки: OrdersUnitTest, OrdersIntegrationTest
Ризик: низький
Такий висновок корисний саме тому, що він короткий і перевірний. Reviewer не читає роман на пʼять екранів — він бачить: контракт на місці, перевірки прогнали, зміна зворотна.
6. Анатомія зрозумілого PR із рефакторингу
Багато проблем архітектурного рефакторингу виникають не тому, що код поганий, а тому, що PR погано упакований. Автор знає, що «лише виніс publisher». Reviewer бачить три файли, нове імʼя, старе, пару тестів — і нервує, чи не заховали під виглядом cleanup маленьку революцію. Секція «що не змінювалося» заспокоює його найшвидше. Зміна маленька — один commit; кроків два — вони окремо зворотні, але живуть в одному PR, а не розмазуються по історії, мов серіал на шість сезонів.
І ось тут уся схема нарешті складається в одне ціле. Harness дає технічне підтвердження. Lightweight characterization та наявні integration tests страхують поведінку. Incremental loop захищає від broad rewrite. Критерії приймання перетворюють усе це на PR, який не вимагає від reviewer телепатії.
Якщо reviewer відкриває такий PR і за один прохід розуміє: бізнес-сервіс більше не знає про Kafka напряму, REST не змінився, event не змінився, тести зелені, відкат дешевий — значить, ви зробили рівно те, чому присвячена ця лекція. Не ефектне «переродження архітектури», а спокійне, точне й зворотне поліпшення системи — іноді це цінніше за все інше, бо саме так код стає кращим без зайвої драми.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ