Зміст
П’ятниця, шоста вечора. У модулі комерційної пропозиції треба змінити знижку одному клієнту. Файл на вісімсот рядків. Поруч із формулою ціни там перевірка прав, запит до бази, лист менеджеру і збирання відповіді для браузера. Ви правите три рядки. Перевірки щасливого шляху зелені. У понеділок бухгалтерія бачить чужу знижку на іншому договорі.
Кухню, де вже стоїть вечеря, у суботу не зносять. Переставляють одну шафу, підписують полиці і лишають плиту працювати. З кодом та сама дисципліна. Рефакторинг рухає внутрішній устрій і не змінює того, що бачить користувач. Декомпозиція вирішує, яка полиця за що відповідає. Переписування з нуля виселяє дім у готель на рік, і майже завжди з’ясовується, що на старій кухні жили правила, яких немає в жодному документі.
Далі про те, як відрізнити ці три роботи, де проводити межу і як пройти шлях на одному перевантаженому модулі, не зупиняючи систему. Іменовані кроки Фаулера розібрані у вижимці «Рефакторингу». Тут рішення: коли вистачає функції, коли потрібен модуль, коли виправданий окремий сервіс, і де агент допомагає, а де підміняє зміст.

Ключові висновки
Рефакторинг, декомпозиція і переписування — три різні рішення. Перше зберігає спостережувану поведінку і змінює форму. Друге проводить межі відповідальності. Третє змінює модель або технологію і мусить довести, що старі правила не загубилися.
Довгий файл сам по собі нічого не доводить. Погано, коли одна зміна бізнес-правила зачіпає права, зберігання, листи і формат відповіді. Добре, коли рідкісний сценарій лежить довгим, але локальним шматком, який чіпають раз на рік.
Спершу фіксують фактичну поведінку, потім рухають стіни. Якщо вимоги загублено, характеризувальні тести записують те, що система робить зараз, разом з історичними дивностями. Дефект і «так історично склалося» розділяють явно, до перенесення коду.
Межу ставлять навколо напрямку змін, а не навколо технічних тек. Модуль сильний, коли його обов’язки тісно пов’язані між собою, і слабкий, коли кожен сусід лізе всередину. Фізичне рознесення по сервісах має сенс лише після такої межі.
Маленький перевірюваний крок тримає систему живою. Один логічний прийом, прогін перевірок, окремий коміт на структуру і окремий на поведінку. Агент може виконати механіку. Еквівалентність поведінки і зміст правила лишаються на інженерові.
Три роботи, які плутають в одному запиті

У запиті на злиття часто лежить усе одразу: перейменували поля, «заодно» змінили порядок знижок і винесли половину файлу в новий каталог. Рев’юер не бачить, де форма, а де гроші клієнта. Кухня в тому самому стані, якщо за одну суботу переставили шафу, переписали рецепт борщу і замовили нову плиту. Гості не зрозуміють, від чого зіпсувалася вечеря.
Рефакторинг зберігає те, що зовні
Рефакторинг змінює внутрішній устрій за тієї самої спостережуваної поведінки. Користувацький сценарій, бізнес-результат, відповіді на тих самих вхідних даних лишаються попередніми. Це вже не «причісування»: косметична правка імен без межі і без перевірки легко маскує зсув формули.
Практична ознака проста. Після структурного коміту набір характеризувальних перевірок дає ті самі результати, що й до нього. Якщо результат змінився, ви вже в іншій роботі, навіть якщо повідомлення коміту говорить про порядок у файлі.
Каталог прийомів потрібен як спільна мова команди. «Витягти функцію», «увести об’єкт параметрів», «сховати змінювану змінну» — це імена рухів, а не справа смаку. Детальний розбір двох капелюхів, запахів і кроків живе у вижимці книжки Фаулера. Тут важливий наслідок: у один момент ви або рухаєте стіни, або змінюєте рецепт.
Декомпозиція називає, хто за що відповідає
Декомпозиція відповідає на питання, хто за що відповідає. Ви виділяєте самостійні обов’язки, називаєте межу і вирішуєте, що зовні видно, а що сховано. Фізичний розріз на файли, пакети чи сервіси може збігтися з цією межею. Може і не збігтися: два класи в одному файлі з вузьким викликом між ними вже декомпозиція, якщо сусіди не лізуть у чужі поля.
Плутанина починається, коли «рознести по теках» вважають архітектурою. Теки повторюють технічні шари: контролери, служби, сховища. Правило знижки при цьому розмазане по трьох теках і змінюється в трьох місцях. Межа пройшла по типу файлу, а не по причині зміни.
Коли структури вже мало
Рефакторингу перестає вистачати в чотирьох випадках.
Модель предметної області бреше. Знижка, ліміт і податок сидять трьома непов’язаними числами, і жодне ім’я в коді не збігається з тим, як це називає комерція.
Архітектура не відповідає поточному навантаженню. Модуль, який рахував одну пропозицію на хвилину, тепер рахує пакети для вітрини, і синхронний лист усередині розрахунку гальмує касу.
Технологія вперлася в стелю, яку не прибрати перестановкою функцій. Тут уже розмова про заміну компонента, а не про красу методу.
І четверте: правила так переплетені з випадковими деталями зберігання, що локальний прийом лише переносить клубок. Тоді виправдане поступове перепроектування одного контуру. Знесення всієї системи виправдане рідко і лише для шматка з зрозумілою межею і планом звірки. Як це виглядає на масштабі корпоративної системи, розібрано в плейбуці поступового заміщення.

| Питання | Рефакторинг | Декомпозиція | Переписування компонента |
|---|---|---|---|
| Що змінюється | Форма коду | Межі відповідальності | Модель або технологія |
| Поведінка | Та сама | Та сама, поки не вирішите інакше | Нова, її треба звірити зі старою |
| Ознака готовності | Ті самі результати перевірок | Сусід викликає вузький контракт | Розбіжності класифіковані до перемикання |
| Типовий провал | Формула поїхала в тому самому коміті | Теки замість меж | Рік паралельного життя двох правд |
Довгий файл ще не діагноз

Команда приносить звіт статичного аналізу: метод на чотириста рядків, цикломатична складність за порогом, дублі у двох службах. Звіт корисний як карта. Діагнозом він стає лише разом із питанням, що боляче змінювати цього тижня.
Симптоми, які відчуває той, хто править
Велика функція, яка і перевіряє права, і рахує, і пише в базу, і шле лист. Дублі одного бізнес-правила: стеля знижки живе в розрахунку пропозиції і ще раз у нічному звіті, і вони вже роз’їхалися на відсоток. Ланцюжок умов, куди страшно додати гілку, бо сусідні гілки тримаються на порядку перевірок. Цикл залежностей: модуль цін імпортує рахунки, рахунки імпортують ціни. Одна функція продукту змушує відкрити дванадцять файлів. Маленька правка дає побічний ефект у місці, якого ви не чіпали.
Будь-яка з цих ознак окремо ще терпима. Разом вони означають, що вартість наступної зміни непередбачувана. Це і є податок, на який перетворюється відкладена структура. Ціна такої несподіванки в проді розібрана в економіці відмови.
Запахи, яким варто дати ім’я
Імена потрібні, щоб у рев’ю говорити про структуру, а не про смак.
Божественний модуль. На один тип звалено все: транспорт, правила, зберігання, листи. Будь-яке завдання проходить через нього, і він знає забагато про сусідів.
Правка дробом. Одна бізнес-зміна вимагає синхронних правок у різних кінцях системи. Ім’я прийому у Фаулера — Shotgun Surgery. Українською досить фрази: правило не лежить в одному місці.
Заздрість до чужих даних. Логіка модуля А постійно читає внутрішні поля модуля Б і майже не користується своїми. Отже, поведінка хоче жити поруч із даними Б. Ім’я прийому — Feature Envy.
Одержимість примітивами. Договір, гроші і період представлені розсипом рядків і чисел без власного типу. Перевірки «це точно ідентифікатор, а не сума» розповзаються по викликачах. Ім’я прийому — Primitive Obsession.
Протікаюча абстракція. Зовні обіцяли «порахувати пропозицію», а назовні стирчать деталі таблиці і коди помилок драйвера бази. Сусіди починають залежати від того, що мало лишитися всередині.
Зчеплення тут просте: модулі знають одне про одного більше, ніж потрібно для виклику. Зв’язність — зворотний бік. Усередині модуля обов’язки або про одну й ту саму зміну, або це випадкові сусіди по файлу.
Коли компроміс нормальний
Довгий файл з одним сценарієм міграції, який запускають раз на рік, може бути чеснішим за дюжину крихітних типів. Дубль двох схожих формул іноді дешевший за спільну абстракцію: за квартал правила роз’їдуться, а спільний метод обросте прапорцями. Проста умова на три гілки краща за передчасну ієрархію класів. Універсальний каркас «на всі майбутні канали продажів» майже завжди кодує канал, якого ще немає.
Рішення спирається на три питання. Як часто це місце змінюється? Яка ціна помилки? Скільки коштує супровід тієї форми, яку ви хочете ввести сьогодні? Рідкісний код з низькою ціною помилки лишають явним і навіть дубльованим. Часте правило з високою ціною помилки заслуговує на ім’я, тип і окрему перевірку.
Спершу запишіть, як система поводиться зараз

Перестановка шафи при увімкненій плиті починається з інвентаризації. Які каструлі реально стоять на вогні, а які числяться в старому списку покупок. У коді те саме. Поки ви не знаєте фактичний шлях від входу до результату, будь-яка «чистіша» структура ризикує викинути виняток, на якому тримається звіт.
Пройдіть шлях даних
Візьміть один живий сценарій, краще той, який болить. Від точки входу (запит, повідомлення черги, нічне завдання) дійдіть до запису, відповіді і побічних дій. Позначте зовнішні залежності: база, пошта, платіжний шлюз, сусідній модуль. Випишіть правила, яких немає у вікі: «золотому клієнту знижка лише якщо сума більша за поріг і промокод не застосований». Намалюйте, хто кого викликає. Карта на одну сторінку корисніша за схему всієї системи, яку ніхто не оновлює.
Дорогою виринають точки, де модуль знає забагато. Перевірка прав усередині формули ціни. Формат листа, зібраний з полів таблиці. Це майбутні шви. Їх не ріжуть тієї самої години, коли знайшли. Спершу їх називають.
Характеризувальні тести тримають стару поведінку
Майкл Фізерс у книжці «Ефективна робота з успадкованим кодом» (Working Effectively with Legacy Code) описав прийом для системи, у якої вимоги загублено. Характеризувальний тест фіксує те, що код робить зараз, а не те, що він на вашу думку має робити. Ви подаєте вхід, записуєте вихід і закріплюєте цю пару. Після структурного кроку пара мусить збігтися.
Такі перевірки пишуть на тому шві, який реально можна захопити. Якщо модуль не дає викликати розрахунок без бази і пошти, перший крок часто не «покрити все юнітами», а витягнути чисту формулу так, щоб її можна було викликати з числами. Поки формулу не відокремлено, характеризувальний тест може бути тонким зрізом через точку входу: одне замовлення, одна знижка, одна помилка клієнта, один граничний нуль.
Окрема розвилка: це дефект чи історична поведінка? Якщо звіт за минулий рік зійшовся саме на дивному округленні, «полагодити заодно» зламає звірку. Запишіть дивність у тест і заведіть окреме завдання на зміну поведінки. Зміна поведінки — інший капелюх і інший коміт.
Метрики, які відповідають на вартість зміни
Покриття тестами показує, які гілки ви вже притиснули. Воно не показує, чи легко додати нове правило. Поруч дивіться на речі, які відчуває команда.
Скільки модулів доводиться відкрити для типової правки. Наскільки глибоко вкладені умови в гарячій функції. Скільки часу забирає типова зміна від завдання до перевірки. Як часто після маленької правки приїжджає регресія. Наскільки боляче повторити перевірку руками, якщо автоматичної немає.
Цикломатичне число і довжина методу лишаються підказками, не ціллю. Рефакторинг заради того, щоб аналізатор замовк, легко плодить функції по три рядки без нового шва. Ціль інша: наступна зміна того самого правила зачіпає менше місць і перевіряється швидше.
Межу проводять там, де код змінюється разом

Принцип єдиної відповідальності корисний як питання, а не як квота «один клас — один метод». Запитайте, з якої причини цей шматок змінюється. Якщо причини дві і вони приїжджають від різних людей у різні тижні, шматок суміщає ролі. Якщо причини завжди приїжджають разом, різати рано.
Для прикладного модуля ролі зазвичай такі. Транспорт приймає запит і віддає відповідь. Прикладний сценарій збирає кроки одного випадку. Бізнес-правила рахують гроші і заборони. Доступ до даних читає і пише сховище. Зовнішні інтеграції шлють листи і ходять у чужі служби. Коли одна функція робить три ролі з цього списку, вона вже просить шов, навіть якщо файл ще короткий.
Групуйте за напрямком змін
Теки за технічним типом зручні, поки правил мало. Щойно знижка, податок і ліміт починають жити своїм життям, код варто збирати навколо цих правил. Усередині такого модуля зв’язність висока: типи, перевірки і формули змінюються одним завданням. Між модулями зчеплення низьке: сусід передає вже порахований результат або просить операцію за іменем, а не лізе в таблицю.
Практичний тест межі. Назвіть зміну, яка прийде в наступному кварталі. Якщо для неї ви чіпаєте один модуль і його публічний контракт стабільний, межа на місці. Якщо для неї ви шукаєте одну й ту саму константу в чотирьох каталогах, межі немає, скільки б інтерфейсів ви не завели.
Від функції до модуля, без стрибка через сходинки

Порядок зазвичай такий, і перестрибувати дорого.
Спершу чисті функції: на вході числа і рішення, на виході гроші і причини відмови, без бази і мережі. Потім витягання методів з довгої процедури, щоб імена збіглися з правилами. Потім тип предметної області, якщо кілька функцій тримають одні й ті самі інваріанти. Потім явний контракт для сусідів. Внутрішні деталі зберігання і пошти за цей контракт не виглядають.
Інтерфейс заводять, коли є друга реалізація або жорстка межа, яку треба підмінити в перевірці. Інтерфейс на кожен клас без другої реалізації і без шва для тесту додає файли і не знижує зчеплення.
Принципи без догми
SOLID і сусідні формулювання корисні як словник обмежень. Єдина відповідальність питає про причину зміни. Відкритість до розширення попереджає, що нова гілка знижки не повинна переписувати старі гілки, якщо правила справді незалежні. Інверсія залежностей нагадує, що розрахунок не повинен сам відкривати з’єднання з базою: з’єднання приходить ззовні.
Композиція замість успадкування рятує від ієрархій «золотий клієнт, золотий клієнт з промокодом, золотий клієнт з промокодом і ручною знижкою». Набір маленьких правил, застосованих по черзі, простіший за дерево класів на кожну комбінацію.
Патерни з вижимки банди чотирьох підключають, коли в коді вже є повторюваний біль, який патерн називає. Заміна умов поліморфізмом виправдана, коли гілки стабільно означають різні поведінки і плодяться. Три гілки в одній функції поліморфізму не просять.
| Що болить | Найближчий шов | Чого не робити того самого дня |
|---|---|---|
| Функція на десятки рядків з однією причиною змінюватися | Витягти функції поруч | Новий сервіс і спільна бібліотека |
| Кілька правил змінюються разом і їх кличуть ззовні | Модуль з вузьким контрактом | Інтерфейс на кожен внутрішній тип |
| Дві реалізації однієї операції або потрібен шов для тесту | Контракт і підміна | Ієрархія на всі майбутні варіанти |
| Контракти вже є, випуск і відмови спільні | Модульний моноліт, заборона зайвих імпортів | Мережа між модулями «на виріст» |
| Різний цикл випуску, масштаб або команда | Окремий сервіс на цій межі | Спершу розрізати весь застосунок |
Крок, який можна відкотити за хвилину

Добрий прийом вміщується в голові і в дифі. Якщо ви не можете сказати, яка поведінка мусить лишитися попередньою, крок завеликий.
Базові рухи
Extract Function виймає шматок з іменем правила. Inline Function повертає обгортку, яка нічого не ховає і лише заважає читанню. Перейменування і зміна сигнатури роблять намір видимим: total стає totalAfterDiscounts. Introduce Parameter Object збирає розсип аргументів, які завжди їдуть разом, в один тип. Encapsulate Variable ховає змінюване поле, щоб сусіди перестали писати в нього в обхід правила. Безіменні пороги (1000, 0.9) отримують імена, коли зміст не очевидний з місця.
Replace Conditional with Polymorphism лишають на потім. Спершу умова має стабілізуватися як набір справжніх варіантів, а не як тимчасовий список акцій.
Дублювання бізнес-правила і дубль тексту
Шукайте повтор змісту, а не повтор рядків. Дві функції рахують знижку золотого клієнта і вже розходяться на округленні. Це одне правило, яке має жити в одному місці. Дві функції схожі за циклом, але одна рахує комерційну пропозицію, а інша — складський резерв. Спільний «універсальний калькулятор» з десятком прапорців склеїть різні змісти. За місяць прапорець, доданий для складу, змінить ціну в пропозиції.
Лишайте незалежні реалізації, поки не назвете спільне правило однією фразою без «і» і без прапорця. Якщо фраза не будується, спільного правила немає.
Залежності зробіть явними
Адаптер ховає чужий протокол за словами вашої області: не «викликати поштовий SDK», а «повідомити, що пропозиція готова». Фасад збирає пачку внутрішніх викликів в одну операцію для сусіда, якому не потрібні проміжні кроки. Глобальне з’єднання, глобальний користувач і глобальний поштовик замінюються аргументами. Тоді характеризувальний тест підставляє пам’ять замість бази і записує листи в список, а не на реальну скриньку.
Побічні ефекти, які раніше траплялися «десь усередині», стають рядками сценарію: перевірити права, порахувати, зберегти, повідомити. Порядок видно. Його можна змінити окремим комітом поведінки, коли бізнес справді цього просить.
Порядок, який не змішує капелюхи
Спершу перевірка, яка падає, якщо поведінка поїде. Потім один структурний рух. Потім прогін. Потім коміт, у повідомленні якого немає слів про нову функцію. Поведінка, якщо вона взагалі потрібна, приїжджає наступним комітом, і диф там короткий.
Відкат такого коміту повертає попередню форму, а не суміш із трьох завдань. У гілці це виглядає нудно. Нудьга тут і є спосіб не зупиняти плиту.
Цикл на одне завдання, який можна повісити в опис роботи:
- Назвіть конкретний біль, а не «привести модуль до ладу».
- Знайдіть шматки, які змінюються разом.
- Простежте зовнішні контракти і побічні дії.
- Зафіксуйте поточну поведінку тестами на цьому шві.
- Оберіть мінімальну межу, яку готові перевірити сьогодні.
- Зробіть один структурний рух.
- Проженіть тести і статичні перевірки.
- Подивіться, чи не виліз ефект у сусідньому сценарії.
- Повторюйте, поки типова правка не стане локальною.
- Запишіть рішення: яка межа тепер вважається публічною і що не ріжемо наступного разу.
Знесення і новобудова програють прихованим правилам

Переписати модуль з чистого аркуша хочеться, коли файл соромно відкривати. Сором поганий замовник. У старому коді сидять винятки, про які пам’ятає одна людина в комерції: нульова ціна для внутрішнього переміщення, знижка, яку заборонено підсумовувати з промокодом, лист, який мусить отримати не клієнт, а куратор регіону. Поки нова версія пишеться, стара продовжує обростати такими винятками. До моменту перемикання порівнюють уже не дві реалізації однієї формули, а два різні світи.
Друга ціна — подвійне життя. Стару версію не можна вимкнути, поки нова не збіжиться на бойових випадках. Команда платить за обидві. Розбіжності виринають пізно, якщо звірку відклали на «після запуску».
Тому виграшний рисунок інший. Беруть обмежену ділянку зі зрозумілим результатом, наприклад лише розрахунок суми без листів і без прав. Стару реалізацію ізолюють за контрактом, щоб сусіди перестали кликати її нутрощі. Відповідальність переносять маленькими шматками. Сумісність зі старою відповіддю тримають, поки споживачі не перейшли. Старий код видаляють, коли на нього не лишилося викликів, а не коли «нова архітектура загалом готова».
Прийом поступового заміщення, в літературі Strangler Fig, на рівні модуля виглядає так. Новий розрахунок стоїть поруч. Виклики йдуть через розвилку: частина запитів рахує старий шлях, частина — новий, відповіді порівнюють. Розбіжність пишуть у журнал з ідентифікатором пропозиції, а не лагодять на око в п’ятницю. Коли розбіжності зрозумілі і або визнані дефектом, або прийняті як зміна правила, розвилку прибирають. Де цей рисунок обов’язковий на рівні контурів ERP і де він не підходить, див. плейбук заміщення. На одному модулі він не підходить, якщо немає способу порівняти результати і немає шляху відкату розвилки.
Для критичного розрахунку корисні три запобіжники. Подвійний прорахунок без подвійного запису: обидві реалізації рахують, у базу пише ще стара, різниця видна в спостереженні. Тіньовий прогін: новий шлях виконується на копії входу і не впливає на відповідь користувачеві. Перемикач увімкнення: новий шлях умикають на частку трафіку і вимикають без викладки, якщо помилки поповзли. План відкату називають до ввімкнення, а не під час розбору.
Сервіс з’являється після межі
Добра декомпозиція часто закінчується модульним монолітом. Застосунок один, процес один, викладка одна. Усередині модулі з публічними контрактами, заборона на імпорт чужих нутрощів і свої таблиці, які зовні не читають напряму. Це і проміжна зупинка дорогою до сервісів, і самостійна архітектура, якщо в команди немає різних циклів випуску. Чому моноліт роздувається, коли меж немає, розібрано в статті про моноліти ERP. Як узагалі еволюціонують шари вебзастосунку, див. історію архітектури застосунків.
Сервіс виділяють, коли збіглося кілька умов. Життєвий цикл і масштаб справді інші: розрахунок пропозицій має пережити пік вітрини, а листи можуть зачекати. Вимоги до відмов інші: падіння пошти не повинно валити касу. Команда випускає цей контур окремо і готова тримати контракт. Мережа, повторні запити, таймаути і часткова відмова стають частиною роботи, а не несподіванкою після розрізання.
Ціна розподіленої схеми конкретна. Мережа губить пакети і додає затримку. Транзакція, яка раніше була одним комітом бази, роз’їжджається на два сховища і питання узгодженості. Контракт треба версіонувати, бо сусіда не можна оновити тієї самої секунди. Спостереження, журнали і розбір інцидентів коштують дорожче, ніж стек одного процесу.
Порядок рішень тому короткий. Спершу логічна межа і контракт усередині одного процесу. Потім замір: чи боляче жити зі спільним випуском. Лише потім мережа. Розрізати по теках «бо так роблять» означає винести ту саму кашу на два репозиторії і додати таймаути.
Наскрізний приклад: модуль комерційної пропозиції
Умовний модуль робить усе одразу. Приймає тіло запиту. Перевіряє право quote:create. Читає клієнта з бази. Рахує суму рядків. Застосовує знижку золотого рівня і промокод. Не дає сумі піти нижче нуля. Пише пропозицію. Шле лист. Віддає рядок із двома знаками після коми.
Нижче стиснутий вихідний вигляд на TypeScript. Він навмисно плаский: так виглядає шов до роботи.
export async function createQuote(req, db, mailer) {
const user = req.user;
if (!user?.permissions.includes('quote:create')) {
throw new Error('forbidden');
}
const customer = await db.query(
'select id, tier, email from customers where id = $1',
[req.body.customerId],
);
if (!customer.rows[0]) throw new Error('customer');
let total = 0;
for (const line of req.body.lines) {
total += line.qty * line.unitPrice;
}
if (customer.rows[0].tier === 'gold' && total > 1000) total *= 0.9;
if (req.body.promo === 'SPRING' && total > 500) total *= 0.95;
if (total < 0) total = 0;
await db.query('insert into quotes(customer_id, total) values ($1, $2)', [
customer.rows[0].id,
total,
]);
await mailer.send(customer.rows[0].email, `Quote ${total}`);
return { status: 200, body: { total: total.toFixed(2) } };
}
Крок 1. Назвати ролі
Тут п’ять ролей: транспорт і права, сценарій «створити пропозицію», правила ціни, запис у базу, сповіщення. Поки вони в одній функції, будь-яка зміна знижки ризикує зачепити лист і SQL.
Крок 2. Зафіксувати поведінку
Характеризувальні випадки, які варто записати до перенесення формул: клієнт без права; невідомий клієнт; сума рядків без знижки; золотий рівень вище порога; промокод вище свого порога; обидва правила одразу; нуль і від’ємна проміжна сума, якщо вона взагалі можлива на вході. Очікувані числа знімають з поточного коду, а не з нового «правильнішого» округлення.
Крок 3. Вийняти правила, не змінюючи формул
Чиста функція не знає про запит і пошту. Пороги і коефіцієнти поки ті самі, що в старому коді. Імена кажуть правило.
const GOLD_MIN = 1000;
const GOLD_RATE = 0.9;
const PROMO_MIN = 500;
const PROMO_RATE = 0.95;
export function priceQuote(input: {
linesTotal: number;
tier: string;
promo?: string;
}): number {
let total = input.linesTotal;
if (input.tier === 'gold' && total > GOLD_MIN) total *= GOLD_RATE;
if (input.promo === 'SPRING' && total > PROMO_MIN) total *= PROMO_RATE;
if (total < 0) total = 0;
return total;
}
Формули не покращують у цьому коміті. Якщо золота знижка і промокод не повинні перемножуватися, це зміна поведінки і окреме завдання зі звіркою звітів.
Крок 4. Ізолювати зовнішнє
Сховище і сповіщення стають контрактами. У перевірці їх підміняють списками. У застосунку за ними старі SQL і пошта.
export interface QuoteStore {
findCustomer(id: string): Promise<{ id: string; tier: string; email: string } | null>;
insertQuote(customerId: string, total: number): Promise<void>;
}
export interface QuoteNotifier {
quoteReady(email: string, total: number): Promise<void>;
}
Крок 5. Лишити зовні один сценарій
Прикладна функція оркеструє ролі. Транспорт лише дістає користувача і тіло, перевіряє право і перекладає помилки в коди відповіді. Сусіди більше не бачать поріг 1000.
export async function createQuote(
input: { customerId: string; linesTotal: number; promo?: string },
store: QuoteStore,
notifier: QuoteNotifier,
): Promise<number> {
const customer = await store.findCustomer(input.customerId);
if (!customer) throw new Error('customer');
const total = priceQuote({
linesTotal: input.linesTotal,
tier: customer.tier,
promo: input.promo,
});
await store.insertQuote(customer.id, total);
await notifier.quoteReady(customer.email, total);
return total;
}
Крок 6. Переносити виклики і звіряти
Старий createQuote на час стає тонкою оболонкою над новим сценарієм. Відповіді на характеризувальних випадках порівнюють до видалення оболонки. Якщо десь у системі функцію кликали заради побічного листа без запису, це вирине тут, а не після викладки.
Крок 7. Оцінити, чи стало дешевше змінювати
Дивіться не на число рядків. Дивіться, скільки файлів відкриває правка порога золотої знижки (очікування: один модуль правил). Наскільки легко перевірити правило без бази. Наскільки вузький публічний вхід: ідентифікатор, сума рядків, необов’язковий промокод, назовні число. Скільки часу забирає наступна схожа правка.
Якщо поріг як і раніше зашитий у звіті нічної звірки, декомпозицію модуля пропозиції не закінчено. Правило ще живе другим життям. Це наступне завдання того самого циклу, а не привід заводити мікросервіс.
Де переробка коштує дорожче за код

Великий шматок без проміжних перевірок. Відкотити його вже не можна: у дифі і перейменування, і нова формула.
Абстракція під канал продажів, якого немає в переліку цього року. Каркас обганяє завдання і сам стає кодом, який страшно змінювати.
Інтерфейс на кожен клас. Навігація довша, поведінка та сама, шва для підміни немає.
Спільний метод для правил із різним змістом. Прапорці всередині «універсальної» функції повертають ту складність, від якої йшли, тільки в гіршому вигляді: вона не видна за іменем.
Надто дрібні функції і файли. Читання сценарію перетворюється на похід по двадцяти переходах. Шов має збігатися з правилом, а не з кожним рядком.
Спроба впровадити Clean Architecture або предметно-орієнтоване проєктування одразу на всю систему. Шари з’являються там, де ще немає жодного стабільного правила. Книжкові імена не замінюють карту одного сценарію.
Структура і поведінка в одному коміті. Рев’ю пропускає зсув округлення за перенесенням файлу.
Рефакторинг заради цифри аналізатора. Поріг складності зелений, правити знижку як і раніше боляче.
Робота без знання викликів. Видалений «мертвий» аргумент виявляється частиною зовнішнього контракту нічного завдання.
Агент рухає текст, зміст лишається на людині

Агент корисний там, де робота механічна і результат можна перевірити незалежно. Він погано тримає правила, яких не записано ні в коді, ні в тесті.
Де віддача висока. Розібрати граф імпортів і показати цикли. Знайти повтор порога знижки по репозиторію. Накидати характеризувальні тести за поточними входами і виходами, які ви потім звірите очима. Перейменувати і вийняти функцію в межах одного файлу за явною інструкцією. Показати, хто кличе публічний метод, якщо ви збираєтеся змінити сигнатуру. Запропонувати два варіанти розрізу модуля, які ви порівняєте з напрямком змін.
Де потрібна людина. Чи збігається ім’я priceQuote з тим, як знижку розуміє комерція. Чи лишилася поведінка еквівалентною, включно з дивним округленням. Які побічні дії вважаються частиною контракту, хоча їх немає в типі повернення. Чи можна змінювати схему таблиці і зовнішній формат відповіді. Зелений прогін не скасовує читання дифа: агент охоче «трохи покращує» формулу в тому самому кроці.
Практичний розклад на кілька агентів тримає завдання вузькими. Один досліджує залежності і точки входу і не править код. Інший пропонує характеризувальні випадки і не змінює формули. Третій виконує один названий прийом на обмеженому файлі. Окремий прохід дивиться диф, тести і список заборон: не чіпати контракт відповіді, не змінювати коефіцієнти, не видаляти лист. Агент, якому сказали «приведи модуль до чистої архітектури», майже напевно змішає капелюхи. Як провести межу між генерацією і інженерним рішенням, розібрано в статті про те, де закінчується генерація коду. Контекст роботи кількох агентів в одному контурі див. в агентній інженерії і в розборі ШІ на п’ятнадцятирічному легасі.
Часті питання
Чим рефакторинг і переписування відрізняються на практиці?
Рефакторинг мусить зберегти відповіді системи на тих самих входах. Переписування свідомо будує нову реалізацію і тому починається зі звірки, а не з видалення старого файлу. Якщо під час «наведення порядку» змінилася сума, це вже переписування, навіть якщо зачеплено один модуль.
Чи потрібні тести до будь-якої правки?
На шві, який ви рухаєте, потрібна опора: характеризувальний тест, тонкий зріз через точку входу або хоча б зафіксований приклад запиту і відповіді. Покриття всього репозиторію не є вхідним квитком. Відсутність будь-якої опори на гарячому правилі означає, що перший крок — зробити правило викликаним і записати поточний результат.
Коли дублювання краще за спільну функцію?
Коли схожий текст, а не правило. Дві формули, які сьогодні збіглися на демо-даних і належать різним власникам, краще лишити роздільними. Спільну функцію заводять, коли ви можете назвати одне правило і не потребуєте прапорця «режим складу / режим ціни».
Чи завжди треба дробити довгий клас?
Ні. Дробити варто клас, у якого кілька незалежних причин змінюватися або який ховає чужі ролі. Довгий сценарій з однією причиною і рідкісними правками часто читається краще за розсип типів. Дивіться на вартість зміни, а не на лінійку.
Чим модульний моноліт відрізняється від охайних тек?
У модульному моноліті сусід не імпортує внутрішні типи іншого модуля і не читає його таблиці. Публічна поверхня маленька і названа операціями предметної області. Теки без цього правила лишаються навігацією по технічних шарах.
Коли час виділяти мікросервіс?
Коли межа вже є, а спільний процес заважає: різні вимоги до відмови, різний масштаб, окрема команда і окремий випуск. Якщо межі немає, сервіс розмаже те саме правило по мережі. Спершу контракт в одному процесі, потім розмова про транспорт.
Чи можна рефакторити код, якого ніхто не розуміє?
Можна, але першим продуктом буде карта одного сценарію і характеризувальні тести, а не нова структура. Поки вхід і вихід не записані, рухати стіни рано. Людей, які пам’ятають винятки, варто спитати до видалення дивних гілок, а не після інциденту.
Як не дати агенту переписати модуль цілком?
Ставте один прийом і список заборон у завданні. Просіть диф без зміни коефіцієнтів, текстів помилок і схеми таблиць. Звіряйте характеризувальні випадки окремо від прогону, який агент запустив сам. Завдання «покращ архітектуру модуля» замінюють на «витягни розрахунок суми в чисту функцію, виклики не змінюй».
Що почитати далі
Поруч на сайті лежать матеріали, які закривають сусідні питання: каталог кроків, заміщення контурів, ціну збою і межу між генерацією та рішенням.
Висновок

Архітектура тут не креслення, яке затверджують один раз. Це здатність змінити правило знижки в п’ятницю і не розбудити в понеділок чужий договір. Рефакторинг лишає плиту працювати, поки рухають шафу. Декомпозиція підписує полиці так, щоб наступна людина відчинила одні дверцята. Маленькі кроки зі звіркою дають більше контролю, ніж суботник, після якого ніхто не може пояснити, куди поділася стара каструля.
Цього тижня оберіть одне гаряче правило. Запишіть його поточний результат. Вийміть його у функцію, не змінюючи чисел. Подивіться, скільки файлів відчинить наступна правка. Якщо як і раніше більше одного, межа ще попереду, і це нормальний наступний крок, а не привід зносити модуль.



Коментарі