Капстоун I: инвентаризация запахов
Прежде чем трогать грязный модуль, поставь диагноз: назови каждый запах, найди его и отранжируй по стоимости изменения. Приоритизированная инвентаризация, привязанная к вероятным изменениям, а не порыв убраться, — вот что позволяет доказать, что рефакторинг что-то улучшил.
Тебе достаётся checkout.ts на 180 строк. Он собирает HTML, применяет бизнес-правила, ходит в базу и переключается по типу платежа — всё в одной функции. Твой инстинкт — начать прибираться: переименовать тут, вынести хелпер там, причесать switch. Через час файл выглядит иначе, и ты понятия не имеешь, стало ли лучше, потому что ты ни разу не сказал, что вообще было «хуже».
Этот инстинкт — править раньше, чем характеризуешь, — и есть тот провал, ради слома которого существует этот капстоун. Senior не рефакторит беспорядок по наитию. Сначала он производит инвентаризацию запахов: каждый дефект назван точным словарём, найден в коде и отранжирован по тому, насколько он повышает стоимость следующего изменения. Инвентаризация — это диагноз; рефакторинг — лечение; и делаешь ты их именно в этом порядке, чтобы «я улучшил» стало утверждением, которое можно проверить, а не чувством, которое у тебя есть.
После этого урока ты можешь взять реалистичный грязный модуль и произвести приоритизированную инвентаризацию запахов: назвать каждый запах словарём этого трека (одержимость примитивами, переключатель по типу, разрозненные правки, расходящиеся изменения, низкая связность), указать на точные строки, которые его несут, и отранжировать список по влиянию на стоимость изменения, привязанному к изменениям, которых ты реально ждёшь, — чтобы диагноз предшествовал лечению, а у будущего рефакторинга была база, относительно которой его можно измерить.
Назови запах раньше, чем назовёшь исправление. Расплывчатое «тут грязно» оправдывает любую правку и потому ни одной. Точное имя — одержимость примитивами, разрозненные правки, расходящиеся изменения — фиксирует конкретную силу, повышающую твою стоимость изменения, указывает на известный рефакторинг и позволяет ревьюеру согласиться или возразить. Инвентаризация — это список названных диагнозов, а не список правок, которые тебе хочется сделать.
// Vague: "the checkout file is a mess" → no agreement possible, no plan.
// Precise (one inventory row):
// smell: primitive obsession
// location: total/tax/discount modelled as bare `number`
// why: every site re-derives currency, scale, rounding → bugs disagree
// refactor: introduce a Money value objectДисциплина: каждая строка говорит что (запах), где (строки) и почему это дорого — и никогда не прыгает сразу к диффу.
Найди запах — имя без координат бесполезно. «Где-то есть одержимость примитивами» нельзя ни отревьюить, ни отрефакторить, ни проверить. Инвентаризация называет символ, строки и, что критично, разброс: литерал 0.2 в одной функции локален; то же правило, скопированное в рендеринг, выставление счетов и отчётность, — это разрозненные правки: одно логическое изменение, втиснутое во множество физических правок. Именно разброс превращает маленький запах в дорогой.
// Same rule, different cost — locate the SPREAD, not just one instance.
function renderInvoice(o: Order) { return o.subtotal * 0.2; } // tax here
function emailReceipt(o: Order) { return o.subtotal * 0.2; } // ...and here
function monthlyReport(o: Order) { return o.subtotal * 0.2; } // ...and here
// One business decision ("tax is region-based now") → three edits, miss one → silent bug.Местоположение запаха плюс его разброс — вот что позволяет оценить радиус поражения, а это и есть вход для ранжирования.
Ранжируй по влиянию на стоимость изменения, а не по уродству. Не каждый запах заслуживает одинаковой срочности. Ранжируй каждую строку по вероятность изменения × радиус поражения, если оно придёт. switch (payment.type), разросшийся до пяти веток и продублированный в трёх файлах, в модуле, чья единственная причина существовать — «мы всё время добавляем способы оплаты», — наверху списка: ожидаемое изменение частое, а его радиус поражения широкий. Слегка длинный, но стабильный хелпер, которого не трогали год, ранжируется низко, даже если это самое уродливое на экране, — рефакторить его незачем, потому что изменение не придёт.
// Ranking is a product, not a beauty contest.
// type-switch on payment, ×3 files → likely change: HIGH, blast radius: WIDE → P0
// primitive `Money` everywhere → likely change: MED, blast radius: WIDE → P1
// long render function, stable → likely change: LOW, blast radius: LOCAL → P3Это и есть senior-ход: ты сортируешь беспорядок относительно роадмапа, чтобы усилия легли туда, где следующее изменение реально болит.
Привяжи инвентаризацию к базе, иначе не докажешь, что что-то улучшил. Причина, по которой диагноз идёт раньше лечения, — не бюрократия, а измеримость. Если начать с правок, исходное поведение и исходный список запахов оба исчезают, и у «стало лучше?» нет ответа. Поэтому до любой структурной правки должны существовать две вещи: записанная инвентаризация (чтобы ты знал, какие дефекты целил) и сеть характеризующих тестов, пришпиливающая текущее поведение (чтобы рефакторинг не смог изменить поведение, пока ты «прибираешься»). Инвентаризация говорит, что ты намерен убрать; тесты говорят, что убрал ты только это.
// Baseline first. Pin behaviour you don't yet understand — bugs included — so the
// refactor is provably behaviour-preserving and the smell list is the scorecard.
test("checkout total for a 2-item USD cart, card payment", () => {
expect(checkout(sampleCart, { type: "card" })).toEqual(EXPECTED_SNAPSHOT);
});
// Now each refactor either removes an inventory row and stays green, or it's wrong.Инвентаризация + характеризующие тесты = состояние «до», относительно которого можно рефакторить. Без них рефакторинг неотличим от переписывания по вайбам.
Поставь диагноз этому модулю. Вот реалистичный беспорядок, который вручает тебе капстоун, — сервис заказа/оформления, мешающий представление, бизнес-правила и сохранение, с одержимостью примитивами и растущим switch по платежам, и без тестов:
// checkout.ts — one function does everything
function checkout(cart: any, payment: any): string {
let total = 0;
for (const item of cart.items) total += item.price * item.qty; // money as bare number
total = total + total * 0.2; // tax literal, also in invoice.ts + report.ts
if (cart.coupon) total = total * 0.9; // discount literal, scattered
let fee = 0; // growing payment switch
if (payment.type === "card") fee = total * 0.029 + 0.30;
else if (payment.type === "paypal") fee = total * 0.034;
else if (payment.type === "wire") fee = 15;
// (a new branch gets added here every quarter)
db.query(`INSERT INTO orders VALUES (${total}, '${payment.type}')`); // persistence inlined + SQLi
return `<div class="receipt">Total: $${(total + fee).toFixed(2)}</div>`; // HTML built inline
}Инвентаризация — названо, найдено, отранжировано:
P0 type switch on payment.type checkout.ts:9–12 (+ retry.ts, analytics.ts)
why: the module's whole job is "add payment methods"; every new method edits N files
→ replace-conditional-with-polymorphism (a PaymentMethod per type)
P0 mixed responsibilities / low cohesion the whole function
why: presentation + rules + persistence in one place → 3 unrelated actors edit one function (divergent change)
→ split into compute / persist / render seams (SRP)
P1 primitive obsession on money total/tax/discount as bare `number`
why: currency, scale, rounding re-derived everywhere; tax 0.2 duplicated → shotgun surgery on a rate change
→ Money value object; one tax/discount boundary
P2 SQL injection + inlined persistence checkout.ts:14
why: a real bug, but isolated to one line; parameterize now, extract the repository during the refactor
P3 `any` types on cart/payment signature
why: no compiler help, but stable shape; type after seams existЗаметь, чего инвентаризация не делает: она не начинает править. Она фиксирует — на бумаге, — какие дефекты существуют, где и в каком порядке их уберут. P2 (SQLi) — настоящий баг, но для последовательности рефакторинга он ранжируется ниже структурных P0 (инъекцию ты всё равно закрыл бы немедленно как фикс, отдельно от рефакторинга). Этот упорядоченный список плюс характеризующий тест, фиксирующий текущий вывод <div> и строку в БД, — это база, относительно которой рефакторит следующий урок, и единственное, что позволит тебе потом с доказательством сказать «P0 и P1 убраны, поведение не изменилось».
▸Почему это работает
Почему инвентаризировать сначала, а не рефакторить по ходу чтения? Потому что ранжирование — глобальное решение, а правка — локальное. Читая строку 9, ты ещё не видишь, что тот же налоговый литерал всплывает в report.ts, поэтому правка там была бы преждевременной — ты починил бы локальный экземпляр и упустил бы, что настоящий запах — это разброс. Инвентаризация заставляет обозреть весь радиус поражения до того, как вкладывать усилия, а это ровно та информация, что превращает «сначала самое уродливое» в «сначала самое дорогое». Диагноз дёшев и обратим; лечение — нет. Сделай дешёвый, обратимый, глобальный шаг до дорогого, локального, необратимого.
▸Частая ошибка
Сигнатурный провал: открыть файл и тут же переименовать переменные, вынести хелпер или «просто починить» switch. Это ощущается продуктивным и уничтожает твою базу. Как только ты поправил, исходное поведение и исходный список запахов оба исчезли, поэтому у вопроса «помог ли этот рефакторинг?» нет ответа — ты можешь лишь сказать, что код выглядит иначе. Хуже: посреди правки ты обнаружишь запах, который не планировал, починишь и его, и вот ты уже на три недоделанных рефакторинга в глубину без зелёных тестов. Правило строгое: никаких структурных правок до того, как модуль характеризован (поведение пришпилено) и инвентаризирован (запахи названы, найдены, отранжированы). Диагноз до лечения, всегда.
Тебе вручают грязный модуль checkout и велят привести его в порядок. Роадмап модуля показывает, что новые способы оплаты добавляют почти каждый квартал. Какой единственный самый senior первый ход?
Капстоун-рефакторинг начинается с диагноза, а не правок. Инвентаризация запахов и есть этот диагноз: каждая строка называет запах словарём трека (одержимость примитивами, переключатель по типу, разрозненные правки, расходящиеся изменения, низкая связность), находит его вплоть до строк и разброса и ранжирует по вероятность ожидаемого изменения × радиус поражения — так что продублированный переключатель платежей в модуле, построенном расти способами оплаты, оказывается наверху, а стабильный-но-уродливый хелпер — внизу. Инвентаризацию ты сочетаешь с характеризующими тестами, чтобы существовала база; без неё «я улучшил» нефальсифицируемо. Провал, который предотвращает всё упражнение, — правка по наитию до характеризации, после которой уже не отличить рефакторинг от переписывания. Поставь диагноз, отранжируй относительно роадмапа, затем лечи.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.