JavaRush /Курси /Claude code /Архітектурний рефакторинг і приймання PR

Архітектурний рефакторинг і приймання PR

Claude code
Рівень 20 , Лекція 4
Відкрита

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 не змінився, тести зелені, відкат дешевий — значить, ви зробили рівно те, чому присвячена ця лекція. Не ефектне «переродження архітектури», а спокійне, точне й зворотне поліпшення системи — іноді це цінніше за все інше, бо саме так код стає кращим без зайвої драми.

1
Опитування
Safe refactoring з Claude, рівень 20, лекція 4
Недоступний
Safe refactoring з Claude
Safe refactoring з Claude
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ