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

Мёртвый код и спекулятивная общность

Мёртвый код и спекулятивная общность — устранимые запахи: неиспользуемые параметры, недостижимые ветки, никогда не вызываемые функции и абстракции «на всякий случай». Чистая стоимость без пользы. Лекарство — удалять смело: git помнит, YAGNI вернёт по реальной нужде.

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

Ты открываешь модуль на 400 строк, чтобы внести одну правку. Сорок из этих строк — закомментированная реализация двухлетней давности. Одна функция принимает флаг legacyMode, который всегда false. Есть абстрактный класс BaseExporter ровно с одним наследником плюс параметр strategy, который когда-либо вызывали только со значением "default". Ничего из этого не выполняется. Ничего не делает. Но всё это по-прежнему здесь, у тебя на пути, и требует, чтобы его прочитали и осмыслили, прежде чем ты сможешь тронуть ту строку, ради которой пришёл.

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

Цель

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

1

Мёртвый код — это код, который не может повлиять на вывод программы, — и он всё равно стоит тебе денег. Он встречается в нескольких повторяющихся формах:

// 1. unused parameter — every caller passes something the body ignores
function priceOf(item: Item, _currency: string): number {
  return item.cents / 100; // currency is never read
}

// 2. unreachable branch — the guard above already returns
function classify(n: number): string {
  if (n >= 0) return "non-negative";
  if (n < 0) return "negative";
  return "impossible"; // dead: every number is one or the other
}

// 3. commented-out block — a fossil nobody dares delete
// function oldExport() { ...40 lines... }

// 4. never-called function — exported, imported nowhere
export function legacyMigrate() { /* ... */ }

Вред не в том, что он работает неправильно, — он вообще не работает. Вред в том, что читатель не может знать, что код мёртв, не доказав этого, поэтому каждый такой кусок вынуждает к маленькому расследованию: «используется ли _currency где-то, чего я не вижу? вызывает ли что-нибудь ещё legacyMigrate?» Этот налог платится при каждом чтении, каждым будущим сопровождающим, вечно.

2

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

// one subclass, ever — the abstraction buys nothing
abstract class Exporter {
  abstract format(rows: Row[]): string;
}
class CsvExporter extends Exporter { format(rows: Row[]) { /* ... */ return ""; } }
// no other Exporter exists, none is planned

// a strategy parameter only ever called one way
function render(view: View, strategy: "default" | "fast" = "default") {
  // 'fast' branch was never written; callers never pass it
}

// a config flag wired through three layers, always the same value
const FEATURE_NEW_PIPELINE = false; // shipped false, never flipped

Каждый из этих кусков добавляет косвенность, ветку или объединение типов, которые ты обязан держать в голове, чтобы прочитать код, — и ни один из них ничего не делает. Ты платишь полную стоимость понимания и сопровождения гибкости, получая взамен ноль гибкости, потому что вариация, под которую её строили, не существует.

3

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

Актив — это поставленное поведение, которым ты пользуешься. Гибкость, которой ты не пользуешься, — это непоставленная стоимость. YAGNI — You Aren’t Gonna Need It («тебе это не понадобится») — экономическая формулировка этого: строить под спекулятивное будущее в среднем проигрышно, потому что большинство предполагаемых вариантов будущего так и не наступают, а те, что наступают, редко совпадают по форме с тем, что ты угадал. Ты платишь стоимость владения сейчас, с процентами, за опцион, который обычно так и не исполняешь.

4

Лекарство — удаление, а система контроля версий делает его бесстрашным. Причина, по которой инженеры копят мёртвый код, — страх «а вдруг он снова понадобится?». Git отвечает на это полностью: удалённый код не исчез, он в одном git log / git show от тебя, со всей своей историей и сообщением коммита, объясняющим, зачем он существовал. Закомментированные блоки хуже удаления — они гниют (окружающий API дрейфует, так что ископаемое уже даже не компилируется), они засоряют результаты поиска и не несут истории. Удали их; настоящая версия — в логе.

// before — the fossil and the flag, in the reader's way
function priceOf(item: Item, _currency: string): number { return item.cents / 100; }
// function oldPriceOf(item) { ...40 dead lines... }

// after — what the code actually does, and nothing else
function priceOf(item: Item): number {
  return item.cents / 100;
}

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

5

Режим отказа: удалить действительно нужную точку расширения только потому, что у неё сегодня один вызывающий. «Один наследник» и «один вызывающий» — это симптомы спекулятивной общности, но они также — то, как выглядит корректный, осознанный шов за миг до того, как наступит его второе использование. Интерфейс PaymentProvider с единственным вызывающим и только StripeProvider за ним может быть переусложнением — а может быть границей, о которой твоя команда договорилась в прошлом спринте, потому что интеграция с PayPal стартует в понедельник, и шов несущий для контрактного теста или плагинного API.

Прежде чем удалять, убедись, что это спекулятивно, а не рано:

  • Есть ли конкретный, утверждённый план или уже существующий потребитель (тестовый дублёр, граница публичного API, вторая реализация в работе)? Тогда это ранняя инфраструктура — оставь её.
  • Это «может, захотим как-нибудь это поменять» без второго случая на горизонте, без теста, прорабатывающего шов, и без вызывающего, использующего абстракцию? Тогда это спекулятивно — удали и добавь заново, когда появится реальный случай.

Senior-ход — это не «никогда не абстрагировать» и не «удалять всё с одним вызывающим». Это умение отличить гибкость, за которую ты платишь и не используешь, от шва, который ты вот-вот используешь, и честность в том, на что именно ты смотришь.

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

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

abstract class ReportExporter {
  abstract export(rows: Row[], options?: ExportOptions): string;
}

interface ExportOptions {
  delimiter?: string;   // only ',' ever passed
  encoding?: string;    // only 'utf-8' ever passed
  legacyHeader?: boolean; // always false
}

class CsvExporter extends ReportExporter {
  export(rows: Row[], options: ExportOptions = {}): string {
    const delim = options.delimiter ?? ",";
    if (options.legacyHeader) { /* never executed */ }
    return rows.map((r) => r.cells.join(delim)).join("\n");
  }
}

// the only caller, in one place:
new CsvExporter().export(rows);

Прочти, что на самом деле используется: функция, которая склеивает ячейки запятыми. Всё остальное — абстрактная база, три опции, ветка legacyHeader — спекулятивно. Нет второго экспортёра, нет второго разделителя, нет вызывающего, который передаёт хоть какую-то опцию. Гибкость — чистая стоимость владения: каждый читатель обязан разобраться в ReportExporter, ExportOptions и мёртвой ветке, чтобы узнать, что отчёты склеиваются через запятую.

После — удали неиспользуемую гибкость; оставь ровно то поведение, которое поставляешь.

function exportCsv(rows: Row[]): string {
  return rows.map((r) => r.cells.join(",")).join("\n");
}

exportCsv(rows); // the one caller, now obvious

Абстрактный класс, объект опций и мёртвая ветка ушли. Если второй формат действительно появится, ты заново введёшь шов тогда — и он подойдёт под настоящий второй случай (ты будешь знать, различаются ли форматы разделителем, структурой или потоковостью) вместо того, который угадал. Git по-прежнему хранит старый ReportExporter, если когда-нибудь захочешь посмотреть, как выглядела спекуляция. Прежде чем резать, ты убедился, что нет PDF-экспортёра в работе и нет контракта публичного API, зависящего от ReportExporter; эта проверка и есть разница между удалением спекуляции и ампутацией шва.

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

Почему удаление безопасно, даже когда оно кажется рискованным? Потому что контроль версий превращает удаление в обратимую операцию с бумажным следом. Удалённая функция живёт вечно в коммите, который её убрал; git log -S'legacyMigrate' находит точный коммит, а дифф плюс сообщение говорят, почему она ушла. Держать код «на всякий случай» не покупает тебе ничего, чего git уже не даёт, — и стоит тебе ежедневного налога на чтение. Асимметрия — это весь аргумент: держать спекулятивный код стоит повторяющейся платы, которую вносит каждый читатель при каждом чтении; добавить его заново позже (если вообще) — это разовая плата, внесённая один раз, одним человеком, когда нужда реальна, а требования наконец известны. YAGNI — это просто рациональный ответ на эту асимметрию.

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

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

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

Ты находишь интерфейс PaymentProvider с единственной реализацией StripeProvider. Другого провайдера в коде нет. Каков senior-ход?

Итог

Мёртвый код (неиспользуемые параметры, недостижимые ветки, закомментированные ископаемые, никогда не вызываемые функции) и спекулятивная общность (абстракции, хуки, флаги и параметры «на всякий случай» под будущее, которое так и не наступило) — это устранимые запахи: чистая стоимость — больше читать, сопровождать и тестировать — ради нулевой текущей пользы. Senior-переосмысление в том, что неиспользуемая гибкость — это пассив, а не актив; актив — это поставленное поведение, которым ты пользуешься. Лекарство — удалять смело, потому что контроль версий помнит (удаление обратимо, со всей историей), а YAGNI говорит, что второй случай сам сообщит настоящую форму, когда действительно наступит, — и добавить заново тогда дешевле, чем нести спекуляцию сейчас. Единственный режим отказа, от которого надо беречься: удалить действительно нужную точку расширения только потому, что у неё сегодня случайно один вызывающий. Прежде чем резать, подтверди, что это спекулятивно, а не просто рано, — нет утверждённого второго случая, нет теста, фиксирующего шов, нет публичной границы, зависящей от него. Тогда удаляй уверенно.

Практика

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

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.