JavaRush /Курси /Claude code /Декомпозиція legacy-модуля:

Декомпозиція legacy-модуля: seam-розрізи

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

1. Декомпозиція зменшує радіус наступної зміни

Коли говорять «декомпозиція», багатьом одразу уявляється щось між clean architecture і корпоративною діаграмою на сорок стрілок. На практиці все набагато спокійніше. Забудьте: ідеальна архітектура не потрібна. Потрібно, щоб наступна правка зачіпала менше залежностей, побічних ефектів і місць, які раптово загоряються червоним.

Проблема legacy-модуля рідко в тому, що метод просто довгий. Набагато частіше біда в тому, що один шматок коду знає занадто багато: читає з бази, рахує бізнес-логіку, пише аудит, викликає зовнішній сервіс і дорогою виправляє старий edge case, про який памʼятають лише бухгалтерія та один втомлений тімлід.

Ось типовий приклад із mrr-engine:

import java.math.BigDecimal;
import java.math.RoundingMode;

public BigDecimal calculate(Subscription sub) {
    BigDecimal annualPrice = jdbcTemplate.queryForObject(SQL, BigDecimal.class, sub.getPlanCode());
    auditService.record("mrr-calc", sub.getId()); // побічний ефект
    BigDecimal monthly = annualPrice.divide(new BigDecimal("12"), RoundingMode.HALF_UP);
    return refundPolicy.applyLegacyAdjustment(sub, monthly);
}

На перший погляд код навіть не виглядає жахливим — кілька рядків, а всередині зліплено чотири відповідальності: читання даних, аудит, розрахунок, legacy-корекція refund-логіки. Захочете змінити один розрахунок — все одно продеретеся повз базу, аудит і refund-policy. Занадто багато звʼязків в одній точці.

Декомпозиція — не «нарізати проєкт на папки», а щоб розрахунок знав про розрахунок, читання — про читання, побічний ефект — про побічний ефект. Чим менше код знає зайвого, тим дешевше його змінювати.

2. Seam — місце для безпечного розрізу

Термін seam звучить трохи академічно, але сама ідея дуже земна. Це місце, де ви підміняєте або ізолюєте поведінку, не переписуючи весь модуль навколо. Блискавка на куртці. Куртку іноді треба відкрити, але краще блискавкою, а не ножицями. У legacy-коді ножиць і так вистачає.

Важливо, що не кожна функція автоматично стає seam: метод, намертво повʼязаний із конкретним SQL, системним часом, логером і глобальним станом, точкою розрізу не стане. У хорошого seam три ознаки: зрозумілий вхід і вихід, він ховає нестабільну деталь, його поведінку видно через поточний baseline.

Для CashFlow Dashboard особливо корисні три види seam:

Вид seam Приклад у mrr-engine Чому це хороший кандидат
Чистий розрахунок місячний MRR із річної ціни легко покривається тестом за входом і виходом
Читання даних пошук ціни тарифу за planCode інфраструктурна деталь ховається за інтерфейсом
Побічний ефект запис audit log можна ізолювати і не змішувати з розрахунком

Нижче — дуже простий приклад seam для читання даних:

import java.math.BigDecimal;

public interface PlanLookup {
    BigDecimal annualPriceFor(String planCode);
}

Тепер SQL можна сховати в адаптері, а решта коду працюватиме з абстракцією, а не з конкретним jdbcTemplate:

import java.math.BigDecimal;

public class JdbcPlanLookup implements PlanLookup {
    public BigDecimal annualPriceFor(String planCode) {
        return jdbcTemplate.queryForObject(SQL, BigDecimal.class, planCode); // SQL сховано тут
    }
}

Зверніть увагу, ми не зробили світ кращим цілком. Ми лише перестали тягнути SQL-деталі всередину розрахункового коду. Це скромний крок, але саме з таких кроків складається безпечна модернізація.

3. Архітектурна межа — це відповідальність

Дуже спокусливо оголосити межею модуля папку. Є папка mrr, отже, ось і модуль. На жаль, legacy-код не зобовʼязаний поважати наші папки. Якщо всередині mrr живуть SQL-запити, прямі виклики білінгу, логування, форматування звіту і шматок API-логіки, то папка є, а межі немає. Є просто склад усього підряд.

Межу краще визначати за відповідальністю. Якщо шматок коду відповідає за розрахунок, він не має одночасно відповідати за спосіб отримання ціни тарифу. Якщо шматок відповідає за аудит, він не має вирішувати, як рахується monthly recurring revenue. Щойно ролі розділяються, залежності теж починають випрямлятися: зовнішній шар знає про внутрішній, але не навпаки.

Схематично це може виглядати так:

flowchart LR
    A[API / звіт] --> F[MrrFacade]
    F --> C[CalcCore]
    F --> P[PlanLookup]
    F --> S[AuditSink]
    P --> J[(Legacy SQL)]
    S --> L[(Журнал аудиту)]

У такій схемі CalcCore не знає ні про JDBC, ні про логування. Він отримує вже підготовлені дані та повертає результат розрахунку. Спосіб отримання даних можна змінювати окремо, і формулу — окремо. Це різко знижує вартість змін.

Перевірка дуже проста. Якщо ви змінюєте розрахунок і раптом змушені чіпати код логування, SQL або контролер, значить, межа поки що слабка. Якщо ж можна змінити одну частину, а решті й далі достатньо того самого контракту на вході та виході, межа стала помітно кращою.

І ще одне корисне запитання: у який бік дивляться залежності? Якщо залежність іде від зовнішнього шару до внутрішнього, це нормально. Якщо бізнес-логіка починає залежати від деталей інфраструктури так, що без них не живе, це вже сигнал тривоги. У CashFlow Dashboard якраз вигідно прагнути до того, щоб формула MRR залежала від даних, а не від способу, яким ці дані дістали.

4. Facade, Adapter і anti-corruption

Щойно розмова доходить до декомпозиції, на сцену одразу виходять знайомі актори: Facade, Adapter і anti-corruption layer. Проблема в тому, що в команді їх часто використовують як заклинання. Сказали «потрібен adapter» — усі кивнули, ніхто не зрозумів. Розкладемо по-людськи.

Facade — це стабільний вхід у нерівний legacy-модуль. Ззовні ви отримуєте один зрозумілий інтерфейс, а всередині можна поступово наводити лад. Adapter — це перекладач між старим і новим інтерфейсом. Він потрібен, коли одна сторона говорить однією мовою, а інша — іншою. Anti-corruption layer — це вже не просто перекладач, а захисний екран: він не дає старим моделям і дивним форматам просочитися в новий код.

Невелика таблиця зазвичай знімає половину плутанини:

Інструмент Що робить Коли корисний
Facade
дає один зрозумілий вхід у legacy-модуль коли всередині хаос, а зовні потрібен стабільний контракт
Adapter
перекладає старий інтерфейс у новий коли джерело даних або сервіс незручний для нового коду
Anti-corruption layer
ізолює новий код від legacy-моделей коли старі сутності надто токсичні і не мають вийти назовні

Наприклад, фасад для MRR може виглядати так:

import java.math.BigDecimal;

public class MrrFacade {
    public BigDecimal calculate(Subscription sub) {
        BigDecimal annualPrice = planLookup.annualPriceFor(sub.getPlanCode());
        return calculator.monthlyFromAnnual(annualPrice);
    }
}

Адаптер може забирати LegacyRow і перетворювати його на нормальну структуру, з якою вже працює новий код:

public class LegacyPlanAdapter {
    public PlanInfo load(String planCode) {
        LegacyRow row = legacyRepository.findByCode(planCode);
        return new PlanInfo(row.getCode(), row.getAnnualPriceCents()); // назовні виходить уже нова модель
    }
}

Головне тут — не перестаратися. Іноді anti-corruption layer роблять такої товщини, що його самого час загортати ще в один захисний шар. Достатньо одного інтерфейсу PlanLookup — не будуйте пʼятишарову архітектуру заради краси. Це модернізація, а не архітектурний театр.

5. Розрахунок окремо, побічні ефекти окремо

Найдешевший тип декомпозиції майже завжди не про класи, а про розділення чистої логіки та побічних ефектів: одна частина рахує, інша пише в лог, ходить у базу, дивиться на час, шле подію назовні. Поки вони в одному клубку, будь-який рефакторинг дорогий і нервовий.

Добра новина в тому, що саме такі розрізи зазвичай найкраще тримаються на characterization baseline: винесений в окремий метод чистий розрахунок ганяється на тих самих входах і дає той самий спостережуваний результат, а інфраструктура лишається зовні та змінюється рідше.

Наприклад, чистий розрахунок можна зробити таким:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class MrrCalculator {
    public BigDecimal monthlyFromAnnual(BigDecimal annualPrice) {
        return annualPrice.divide(new BigDecimal("12"), RoundingMode.HALF_UP); // чистий розрахунок
    }
}

А orchestration-шар нехай збирає дані та викликає побічні ефекти окремо:

public BigDecimal calculate(Subscription sub) {
    BigDecimal annualPrice = planLookup.annualPriceFor(sub.getPlanCode());
    BigDecimal monthly = calculator.monthlyFromAnnual(annualPrice);
    auditSink.recordCalculation(sub.getId(), monthly); // побічний ефект окремо
    return refundAdjustment.apply(sub, monthly);
}

Зверніть увагу, ми не зобовʼязані витягувати взагалі все просто зараз. Якщо refundAdjustment тісно завʼязаний на сусідні модулі, спірну legacy-логіку та зовнішні побічні ефекти, його цілком нормально залишити в колонці «поки не чіпаємо». І це не слабкість. Навпаки: хороший рефакторинг часто визначається не тим, скільки ви винесли, а тим, скільки свідомо не торкнули.

6. Claude Code шукає точки розрізу в inspect-режимі

Claude Code особливо корисний не в режимі «зроби красиво», а в режимі «допоможи знайти кандидатів і пояснити ризики». Якщо дати занадто загальну команду, він із задоволенням запропонує вам півкапремонту сервісу, перейменування пʼяти класів і, можливо, духовне оновлення команди. Тому тут важливий суворий inspect-only режим: спочатку аналіз, потім рішення, і лише потім правки.

Гарний запит просить не лише кандидатів на seam, а й докази: де файл, що саме ізолюється, чим це покрито з baseline і чому це можна брати зараз. Окремо корисно просити колонку «не чіпати». Вона заощаджує дуже багато часу.

Наприклад, запит може бути таким:

Досліди модуль `cashflow-dashboard/mrr-engine` без змін у коді.
Знайди кандидати на безпечні seams.
Для кожного кандидата поверни:
- файл і метод;
- що ізолюється: читання даних, побічний ефект або legacy-формат;
- чим це покрито в characterization baseline;
- ризик помилкового винесення;
- статус: "можна брати зараз" або "краще відкласти".

Після цього корисно підключити reviewer-підхід із Workflow Kit уже до самого diff, а tester — до покриття baseline. Тобто Claude спочатку допомагає вибрати точку розрізу, потім інший контекст або агент перевіряє, що diff справді не тягне зайвого. Так Workflow Kit перестає бути абстрактним набором конфігів і починає реально допомагати CashFlow Dashboard на конкретному legacy-зрізі.

І, звісно, варто уникати запитів на кшталт «приведи модуль до ладу». Це майже гарантований шлях до broad refactor. Claude не вміє читати вашу приховану думку «лише трохи». Це «трохи» потрібно написати явно.

7. Ознаки області, яку краще не чіпати

Одна з найдоросліших звичок у legacy-модернізації — уміти чесно казати «не зараз». Зазвичай новачки думають, що хороший інженер зобовʼязаний сміливо йти в найстрашніші місця. На практиці хороший інженер спочатку позначає небезпечні місця на карті й не наступає туди без потреби. Так, звучить менш героїчно. Зате з працюючим продом.

Є проста різниця між хорошим кандидатом на extraction і зоною, яку поки краще залишити в спокої.

Ознака Зазвичай можна брати зараз Зазвичай краще відкласти
Спостережувана поведінка детермінована, легко порівняти залежить від часу, оточення, сторонніх сервісів
Побічні ефекти немає або один зрозумілий кілька записів, транзакції, каскадні виклики
Покриття baseline є fixtures і golden master baseline слабкий або не покриває цей шлях
Радіус впливу один модуль, один контракт чіпляє billing, refunds, звіти і API одразу
Ціна rollback один commit revert ручне відновлення даних або конфігів

На прикладі CashFlow Dashboard це зазвичай означає, що PlanLookup — хороший старт, а refund-handling у середині billing-потоку — поганий. Не тому, що refund не важливий, а тому, що як перший крок він занадто дорогий. Спочатку випрямляють легкі та корисні межі, глибше заходять потім.

І ось тут колонка defer стає особливо цінною. Запис «refund recalculation — не чіпаємо в цій фазі» — не ганьба, а знак, що ви контролюєте scope, а не йдете з цікавості в невідому печеру без ліхтаря.

8. Робоча нотатка з декомпозиції

Коли аналіз уже зроблено, дуже корисно зафіксувати його в короткій робочій нотатці. Не бюрократія, а інженерний чернетковий запис: секція всередині REFACTOR_LOG.md або тимчасовий markdown-файл у гілці. Головне, щоб наступна людина, включно з вами самими через два дні, зрозуміла, де ми бачимо межі й чому.

Приклад такої нотатки може бути зовсім компактним:

# Декомпозиція mrr-engine

## Поточне зчеплення
Розрахунок MRR змішаний із SQL-читанням тарифу, audit log і legacy refund-adjustment.

## Кандидати-шви
- PlanLookup: читання ціни тарифу
- AuditSink: запис аудиту
- CalcCore: monthlyFromAnnual()

## Винесення з низьким ризиком
- PlanLookup
- CalcCore

## Поки не чіпаємо
- refund-adjustment
- cross-module billing sync
- формат звітного API

## Перший slice
1. винести CalcCore
2. сховати SQL за PlanLookup
3. прогнати characterization + golden master

Хороша нотатка такого типу робить важливу річ: перетворює декомпозицію з абстрактної розмови про архітектуру на конкретний, перевірюваний план. У вас зʼявляється не просто думка «тут би розрізати», а карта місцевості: що змішано, де можливий seam, що безпечно винести, а що поки свідомо залишається на місці. Коли такі межі стають явними, наступний крок модернізації вже сприймається не як хірургія в темряві, а як акуратний рух розміченою стежкою.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ