open atlas
↑ К треку
Паттерны и качество кода CP · 14 · 03

Капстоун III: измеряем до и после

Рефакторинг окупился, только если целевые изменения теперь дёшевы. Измерь до и после — правки на изменение, связанность, тестовые швы, связность — через призму стоимости изменения из юнита 00, и знай, когда «достаточно хорошо» бьёт бесконечное золочение. Здесь трек закрывается.

CP Senior ◷ 20 min
Уровень
ОсновыJuniorMiddleSenior

Ты потратил два дня, распутывая модуль заказов. Он выглядит лучше — функции меньше, имена настоящие, граница налога выделена, появился тест-другой. Твой ревьюер задаёт единственный важный вопрос: «Откуда ты знаешь, что стало лучше, а не просто по-другому?» А затем вопрос потяжелее: «Откуда ты знаешь, что ты закончил

«Кажется чище» — не ответ, который даёт senior. Юнит 00 дал тебе призму, которая на это отвечает: главная стоимость ПО — это стоимость его изменения, — поэтому доказательство рефакторинга не в том, как код выглядит, а в том, во что теперь обходится следующее изменение. Этот урок закрывает трек, измеряя именно это и называя правило остановки, которое отделяет законченный рефакторинг от рефакторинга, который никогда не кончается.

Цель

После этого урока ты можешь доказать, что рефакторинг окупился, так, как делает senior: взяв два целевых изменения, вокруг которых был спланирован капстоун — «добавить новый способ оплаты» и «добавить налог по региону», — и измерив их до и после как правки на изменение, связанность, тестовые швы и связность, всё это привязав к призме стоимости изменения из юнита 00. Ты также можешь сформулировать явный критерий остановки, чтобы рефакторинг кончался на «достаточно хорошо для ожидаемых изменений», а не сползал в золочение.

1

Выбери метрику, о которой был весь трек: правки на одно логическое изменение. Эстетика («чище», «приятнее») неопровержима и не то, за что платит кодовая база. Честная мера — та, что назвал юнит 00, — усиление изменения: сколько мест нужно найти и поправить ради одного концептуального изменения и сколько из них легко пропустить. Оцени два целевых изменения по обоим вариантам.

// BEFORE — "add a new payment method (Klarna)"
//   touch PaymentForm.tsx      (new branch in a switch)
//   touch checkout.ts          (new branch in a switch)
//   touch refund.ts            (new branch in a switch)
//   touch reconcile.ts         (new branch — easy to forget → silent bug)
//   → 4 edits, 4 chances to miss one

// AFTER
//   add  KlarnaProvider implements PaymentProvider   (1 new file)
//   register it in the providers map                 (1 line)
//   → 1 localized add + 1 registration, 0 existing switches touched

Число, которое упало — с 4 разбросанных правок до одного локального добавления, — и есть результат. Всё остальное в этом уроке объясняет, почему оно упало.

2

Покажи, что связанность упала, конкретно — посчитай вещи, которые обязаны меняться вместе. «Слабее связано» — это утверждение; коннасценция и направление зависимостей — это доказательство. Раньше четыре модуля были конаскентны строкам с именами способов оплаты: каждый switch обязан был знать каждый тип оплаты, поэтому четыре менялись вместе. После они зависят от одной абстракции PaymentProvider и ничего не знают ни друг о друге, ни о Klarna.

// BEFORE: every site is coupled to the full set of payment names
function fee(method: string) {
  switch (method) {            // checkout.ts knows: card, paypal, klarna…
    case "card":   return 0.029;
    case "paypal": return 0.034;
    // add klarna here AND in three other switches
  }
}

// AFTER: sites depend on the interface; providers are independent
interface PaymentProvider { fee(): number; charge(amt: number): Promise<Receipt>; }
const providers: Record<string, PaymentProvider> = { card, paypal, klarna };

Измеримое падение: число файлов, обязанных меняться вместе ради «нового способа оплаты», ушло с 4 до 0 существующих + 1 новый. Это связанность, выраженная в числах, — не ощущение.

3

Докажи, что появились новые тестовые швы — написав тест, который раньше был невозможен. Шов — это место, где можно подменить поведение, не редактируя тестируемый код. Раньше комиссии жили внутри switch, к которому добирались только прогоняя весь checkout, поэтому тесту на комиссию нужны были фейковая корзина, фейковый пользователь и заглушка сети. После KlarnaProvider.fee() — чистый юнит, который можно проверить напрямую; шов — это интерфейс.

// AFTER — a test that the BEFORE shape could not express in isolation
test("klarna fee is 1.9%", () => {
  expect(new KlarnaProvider().fee()).toBe(0.019);
});

// region-based tax got a seam too — inject the rate table, no globals
test("tax for EU region", () => {
  const tax = taxFor(100, "EU", { EU: 0.2, US: 0.0 });
  expect(tax).toBe(20);
});

Метрика: тестируемые юниты ушли от «логика комиссии достижима только через полный поток checkout» к «комиссия и налог проверяемы в двухстрочных тестах». Страховочная сеть из юнита 10 — это то, что сделало рефакторинг безопасным; эти швы — то, что рефакторинг произвёл.

4

Покажи, что связность выросла — сгруппируй второе целевое изменение и смотри, как оно ложится в один модуль. Связность — это «то, что меняется вместе, живёт вместе». Второе изменение капстоуна — налог по региону — это зонд. Раньше литерал 0.2 был размазан по прайсингу, выставлению счетов и отчётам (низкая связность: одно правило, много домов). После правило живёт в tax.ts, поэтому изменение региона — это одна правка там, и больше ничто не двигается.

// AFTER — one home for the tax rule; region change is local to it
// tax.ts
export function taxFor(amount: number, region: Region, rates: RateTable): number {
  return amount * rates[region];          // ← region logic changes ONLY here
}

Измерь это как правки до/после для «добавить налог по региону»: с 3 разбросанных мест (а пропущенное становится багом) до 1. Высокая связность — это ровно то, что делает ожидаемое изменение дешёвым, а это единственное определение «хорошего», которым трек пользуется с юнита 00.

5

Назови правило остановки, иначе рефакторинг никогда не кончится. Senior-режим отказа здесь — не недо-рефакторинг, а золочение: полировка за пределами окупаемости, добавление гибкости под изменения, которых никто не просил, погоня за идеальной оценкой модуля, который уже достаточно хорош. Рефакторинг закончен, когда ожидаемые изменения дёшевы и локальны, — а не когда код безупречен.

// STOP signal: re-score the two target changes.
//   "new payment method"  → 1 local add        ✓ cheap
//   "region-based tax"     → 1 local edit       ✓ cheap
// Both targets met → STOP. Do NOT now abstract currency, rounding,
// or a plugin system "while we're in here" — no change is asking for them.

Критерий остановки конкретен: назови изменения, которые ты ожидаешь, рефактори, пока они не станут одиночными локальными правками, и остановись. «Достаточно хорошо для ожидаемых изменений» бьёт «идеально», потому что идеально тратит сегодняшний бюджет на изменения, которые могут никогда не прийти, — ровно ту ошибку, о которой юнит 00 предупреждал, говоря, что изменяемость — это ставка, а не абсолют.

Разбор примера

Оцени рефакторинг по таблице, а не по ощущениям. Возьми модуль капстоуна до и после и заполни строку до/после для каждого целевого изменения. Это артефакт, который ты кладёшь в описание PR, — именно он делает «лучше» опровержимым.

// BEFORE — the messy module, scored against the two expected changes
// add new payment method:  edits=4  coupledFiles=4  seams=0  missRisk=high
// add region-based tax:     edits=3  coupledFiles=3  seams=0  missRisk=high

// AFTER — same two changes, re-scored
// add new payment method:  edits=1  coupledFiles=0(+1 new)  seams=1  missRisk=none
// add region-based tax:     edits=1  coupledFiles=0          seams=1  missRisk=none

Прочитай таблицу обратно через призму юнита 00. Правки на изменение упали (усиление изменения исчезло). Связанные файлы упали до нуля существующих (связанность снизилась — модули больше не двигаются вместе). Швы выросли с 0 до 1 для каждого (изменение теперь можно тестировать изолированно). Риск пропуска ушёл с высокого до никакого (не осталось разбросанного места, чтобы забыть). Каждый столбец — это величина стоимости изменения из юнита 00, и каждый улучшился для тех изменений, которые тебя просили сделать дешёвыми.

Теперь дисциплина, которая завершает трек: посмотри на таблицу и остановись. Оба целевых изменения — одиночные локальные правки. Тебя будет тянуть продолжать — обобщённый тип Money, абстракция валюты, налоговый движок на конфиге. Ни одного из этих изменений в таблице нет. Добавить их значит потратить реальный бюджет чтения-и-тестирования, чтобы купить гибкость под изменения, которых никто не запрашивал. Рефакторинг закончен в тот момент, когда ожидаемые изменения дёшевы; «закончен» — это измеренное состояние, а не ощущение, что код наконец-то идеален.

Почему это работает

Почему измерять изменяемость, а не обычные метрики качества — цикломатическую сложность, число строк, процент покрытия? Потому что это прокси, а прокси дрейфуют от того, что тебя на самом деле волнует. У модуля может быть низкая сложность и высокое покрытие, и всё равно он будет адом для изменений, если его единственное правило продублировано по шести файлам; покрытие ничего не говорит про правки-на-изменение. Призма стоимости изменения измеряет стоимость напрямую: выбери ожидаемые изменения, посчитай, во что каждое обходится до и после. Это единственная метрика, которую нельзя обмануть, потому что она и есть цель. Сложность и покрытие — полезные вспомогательные улики, но главное число — «во что теперь обходится следующее ожидаемое изменение?»

Частая ошибка

Классическая ошибка капстоуна — рефакторить вечно. Модуль проходит оба целевых изменения дёшево, но он «ещё не элегантен» — поэтому ты продолжаешь выносить, обобщать, добавлять швы под гипотетические будущие случаи. Это золочение, и оно по-настоящему дорого: каждая спекулятивная абстракция — это ещё код для чтения, ещё одна догадка о будущем, которая, скорее всего, неверна, и (по «неправильной абстракции» из юнита 12) структура, которую труднее откатить, чем дублирование, которое она заменила. Лекарство — заранее зафиксировать изменения, которые рефакторинг обязан сделать дешёвыми, и остановиться в тот миг, когда они станут одиночными локальными правками. Идеально — не планка. Достаточно хорошо для ожидаемых изменений — это планка, и она измерима.

Проверь себя
Викторина

Твой рефакторинг капстоуна делает оба целевых изменения («новый способ оплаты», «налог по региону») одной локальной правкой каждое, с новыми тестовыми швами. Коллега хочет продолжить: вынести обобщённый тип Money и налоговый движок на конфиге, «пока мы тут». По стандарту этого трека, какое решение верное и почему?

Итог

Рефакторинг доказывается так, как юнит 00 велел измерять всё: по стоимости следующего изменения, а не по тому, как код выглядит. Ты закрыл капстоун, взяв его два целевых изменения — «добавить способ оплаты» и «добавить налог по региону» — и оценив их до и после как правки на изменение (усиление изменения упало), связанность (файлы, обязанные двигаться вместе, упали до нуля существующих), тестовые швы (каждое изменение теперь проверяемо изолированно) и связность (каждое правило живёт в одном доме, поэтому его изменение локально). Эти четыре столбца — те же величины стоимости изменения, с которых открывался трек, теперь улучшенные намеренно. А senior-ход, который завершает трек, — это правило остановки: рефакторинг закончен, когда ожидаемые изменения дёшевы и локальны; погоня за идеалом за этой точкой — золочение, трата реального бюджета на изменения, которых никто не запрашивал. Доказывай изменяемость, а не эстетику; останавливайся на «достаточно хорошо». В этом весь трек, измеренный.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.