Стоимость изменения
Код читают и меняют намного чаще, чем пишут, поэтому главная стоимость ПО — это стоимость его изменения. «Хороший код» — не эстетика, а код, чьё следующее изменение дёшево. Этот урок задаёт призму стоимости изменения, которой пользуется весь трек.
Функции, которую ты написал полгода назад, нужна правка поведения на одну строку. Ты открываешь файл, и одна строка превращается в день: логика переплетена с тремя другими обязанностями, тестов нет, поэтому изменить её безопасно нельзя, имена врут о том, что делает код, а «маленькая» правка ломает двух вызывающих, о которых ты не знал. Код работал — он уехал в прод, он крутился в бою. Почему же менять его так дорого?
Потому что работающий код и изменяемый код — разные свойства, и именно второе стоит денег за время жизни системы. Весь этот трек — про второе свойство. Прежде чем любое правило про именование или SOLID обретёт смысл, нужна призма, которая их все оправдывает: главная стоимость ПО — это стоимость его изменения.
После этого урока ты можешь объяснить, почему соотношение чтения и записи делает читаемость экономическим аргументом, а не вопросом вкуса; описать, как связанность заставляет одно логическое изменение «усиливаться» во множество правок; и использовать вопрос «во что обойдётся следующее изменение?» как тест, который ты применяешь к каждому куску кода в этом треке.
Код читают и меняют намного чаще, чем пишут. Функцию ты пишешь один раз. После этого её читают десятки раз — на ревью, при отладке, при онбординге и при каждом будущем изменении. Эмпирически отношение времени на чтение к времени на запись сильно перекошено в сторону чтения. Один этот факт переворачивает цель оптимизации: код, «оптимизированный» под быстрый набор (короткие имена, хитрые однострочники, копипаста), оптимизирован под самое редкое занятие. Код, оптимизированный под быстрое чтение и изменение, оптимизирован под то, что реально съедает бюджет.
Поэтому «читаемость» — не вкусовщина. Это самый дешёвый рычаг на самой большой статье расходов.
Стоимость системы за время жизни определяется сопровождением, а не первоначальной разработкой. Первая версия — малая часть. Большая часть денег уходит на изменения, которые приходят потом: новые требования, исправления багов, адаптация к новой зависимости, масштабирование. Полезная модель — кривая стоимости изменения: насколько дорога для данного кода следующая фича по мере роста системы?
- В коде, который сопротивляется изменениям (высокая связанность, нет тестов, плохие имена), кривая резко загибается вверх: каждая фича стоит дороже предыдущей, потому что каждое изменение тянет за собой всё больше системы.
- В коде, спроектированном под изменения (связные модули, ясные швы, тесты), кривая остаётся почти плоской: фича N стоит примерно столько же, сколько фича N−1.
Дисциплины этого трека — это то, что держит кривую плоской.
Связанность превращает одно логическое изменение во множество физических правок — «усиление изменения». Допустим, бизнес-правило «налог 20%» меняется на «налог зависит от региона». Если правило живёт в одном месте за ясным именем — это одна правка. Если литерал 0.2 продублирован по двенадцати местам вызова или налоговая логика вшита в рендеринг, оформление заказа и отчёты, то то же самое логическое изменение теперь требует найти и поправить их все — а пропустить хоть одно значит получить баг.
Число мест, которые приходится тронуть ради одного концептуального изменения, — прямая мера того, насколько код враждебен к изменениям. Хорошая структура минимизирует его; именно для этого по большей части и нужны связность и принципы SOLID.
«Энтропия ПО»: без активных усилий изменяемость деградирует. Каждая быстрая заплатка, игнорирующая структуру — прикрученный флаг, втиснутый частный случай, копипаста «только разок» — добавляет немного беспорядка. Беспорядок накапливается: чем грязнее код, тем труднее сделать следующее изменение чисто, поэтому и оно становится заплаткой, которая добавляет ещё беспорядка. Предоставленный сам себе, код дрейфует к дорогому концу кривой. Вот почему «приберёмся потом» само редко случается и почему правило бойскаута (оставь код чуть лучше, чем нашёл) — это экономическая стратегия, а не моральная.
Тест, который ты будешь применять весь трек: «во что обойдётся следующее изменение?» Каждое правило далее — раскрывающие намерение имена, маленькие связные функции, единая ответственность, зависимость от абстракций, рефакторинг под тестами — оправдано одним и тем же вопросом. Имя хорошо, если делает изменение следующего читателя дешевле. Граница модуля хороша, если сжимает радиус поражения вероятного изменения. Тест хорош, если позволяет менять код без страха. Когда два дизайна конкурируют, senior-критерий редко «что красивее» — это «что делает ожидаемое следующее изменение дешевле, не переплачивая сейчас за изменения, которые могут никогда не случиться».
Посмотри, как одно логическое изменение усиливается. Вот налоговое правило, продублированное по коду:
// pricing.ts
const total = subtotal + subtotal * 0.2;
// invoice.ts
const tax = lineTotal * 0.2;
// report.ts
const projectedTax = forecast * 0.2;Правило «налог 20%» выражено трижды, как голый литерал 0.2. Когда правило меняется на «налог зависит от региона», измениться должно каждое место — и каждое теперь обязано узнать про регионы, о которых раньше не имело причин знать. Изменение усиливается: одно бизнес-решение, три (или тридцать) правок плюс риск пропустить одно.
Теперь то же правило за единственной границей:
// tax.ts — единственное место, где живёт правило
export function taxFor(amount: number, region: Region): number {
return amount * rateFor(region);
}
// вызывающие зависят от имени, а не от числа
const total = subtotal + taxFor(subtotal, region);
const tax = taxFor(lineTotal, region);
const projectedTax = taxFor(forecast, region);Изменение по региону теперь — одна правка внутри taxFor. Вызывающие не тронуты, потому что зависели от что (налог для суммы), а не от как (умножить на 0.2). Ничего хитрого здесь нет — правило просто названо и размещено один раз. В этом вся игра: расположить код так, чтобы ожидаемые изменения были дешёвыми и локальными. Заметь, что цену заплатили заранее, назвав концепцию; отдача — каждое будущее изменение этого правила.
▸Почему это работает
Почему бы не вынести всё на всякий случай? Потому что изменяемость — это ставка, а не абсолют. Каждая добавленная абстракция сама является кодом для чтения и догадкой о том, какое изменение придёт. Вынести концепцию, которая никогда не варьируется, значит добавить косвенность без покупки гибкости — ты платишь стоимость чтения и не получаешь ничего взамен. Навык не в том, чтобы «всегда абстрагировать»; он в предсказании, какие изменения вероятны, и удешевлении именно их, оставляя маловероятные простыми. Поздние юниты трека (эвристики, «неправильная абстракция») — про это суждение. Пока: призма — это стоимость ожидаемого изменения, а не гибкость ради гибкости.
▸Частая ошибка
Частая ошибка прочтения: «хороший код = код, который работает». Работающий код берёт самую низкую планку — он выдаёт правильный результат сегодня. Он ничего не говорит о том, во что обойдётся следующее изменение. Полно кода, проходящего все тесты, который кошмарно менять, и полно красиво выглядящего, но переусложнённого кода, который менять так же тяжело. Не оценивай код по тому, запускается ли он; оценивай его по стоимости следующего реалистичного изменения. Это единственное определение «хорошего», которым пользуется этот трек.
Две реализации проходят все текущие тесты. Одна вшивает продублированное бизнес-правило по многим файлам; другая называет правило и размещает его один раз. По определению этого трека, какая «лучше» и почему?
Главная стоимость ПО — это стоимость его изменения, потому что код читают и меняют намного чаще, чем пишут, а время жизни системы — это в основном сопровождение. Связанность вызывает усиление изменения — одно логическое изменение становится множеством физических правок, — а энтропия ПО означает, что изменяемость деградирует, если её активно не защищать. Поэтому «хороший код» — не эстетика: это код, чьё следующее реалистичное изменение дёшево и локально. Каждое правило этого трека — именование, связность, SOLID, рефакторинг под тестами — оправдано одним вопросом, который ты теперь должен применять ко всему читаемому коду: во что обойдётся следующее изменение? Оставшиеся уроки этого юнита заостряют эту призму, прежде чем мы начнём называть конкретные силы, повышающие и понижающие стоимость.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.