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

DRY, KISS, YAGNI в противоречии

DRY, KISS и YAGNI — эвристики, а не законы, и они тянут в разные стороны. DRY охраняет единый источник знания, а не одинаковый на вид текст. Два фрагмента, меняющиеся по разным причинам, — не дублирование, и DRY-инг связывает несвязанное.

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

Над столом у тебя приклеены три акронима: DRY, KISS, YAGNI. Каждый повторяют на ревью так, будто он закрывает спор. Потом ревьюер говорит «это дублируется, заDRY-инь это», ты выносишь общий хелпер, и через три недели у этого хелпера появляется флаг mode, затем булев isLegacy, затем опция skipValidation — по одной ветке на каждого вызывающего. Ты выполнил правило и получил кашу.

Так происходит, потому что эти трое — не законы, которые применяют механически. Это эвристики, и хуже того, они конфликтуют: DRY толкает объединять, KISS и YAGNI толкают оставить всё как есть и порознь. Senior-навык — не заучить их, а решить, какая из них доминирует в этом месте, и знать тот режим отказа, который каждая создаёт, когда побеждает там, где не должна.

Цель

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

1

DRY — про знание, а не про текст. Принцип «Don’t Repeat Yourself» в исходной формулировке звучит так: каждый кусок знания должен иметь единственное авторитетное представление в системе. Единица — это кусок знания: бизнес-правило, вычисление, политика, — а не череда символов, которые случайно выглядят одинаково. Два сниппета могут быть байт-в-байт идентичны и при этом не нарушать DRY, потому что они кодируют два разных куска знания, которые лишь совпадают сегодня.

// Both compute "is this value present and non-empty". Same TEXT.
function isValidUsername(s: string): boolean {
  return s != null && s.trim().length > 0;
}
function isValidComment(s: string): boolean {
  return s != null && s.trim().length > 0;
}

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

2

Настоящий тест дублирования: меняются ли они по одной и той же причине? Это операционная форма DRY. Если единственное решение в бизнесе вынудит тебя править оба фрагмента вместе, они кодируют один кусок знания и должны жить в одном месте. Если изменение одного оставит другой корректным, они независимы и обязаны оставаться порознь.

// TRUE duplication: one rule, the 20% VAT rate, written twice.
const invoiceTotal = subtotal * 1.2;      // in invoice.ts
const quoteTotal   = estimate * 1.2;      // in quote.ts

Здесь 1.2 — это один и тот же факт, ставка НДС, выраженная дважды. Когда НДС меняется на 22%, оба должны измениться, вместе, по одной причине. Это настоящее дублирование: назови его один раз (vatRate, applyVat()), и оба места зависят от единственного источника. То, что литералы выглядят одинаково, — побочно; важно, что одно решение управляет обоими. Та же форма может быть совпадением; та же причина для изменения — это дублирование.

3

KISS и YAGNI — противовесы, не дающие DRY выстрелить лишний раз. DRY, взятый сам по себе, имеет очевидное направление отказа: объединяй агрессивно — и свяжешь то, что было независимым. KISS («keep it simple — простейшее, что работает») и YAGNI («you aren’t gonna need it — не строй под нужды, которых у тебя ещё нет») тянут в обратную сторону. Они противостоят DRY не случайно: они существуют именно чтобы его ограничивать.

// YAGNI violation born from "DRY-ing ahead": a generic engine for ONE caller.
function process<T>(
  items: T[],
  opts: { parallel?: boolean; retries?: number; transform?: (x: T) => T; dedupe?: boolean },
): T[] { /* 60 lines of branching nobody currently exercises */ }

// What was actually needed today:
const result = items.map(normalize);

Обобщённый process «DRY» в том смысле, что он мог бы вобрать будущих вызывающих, — но никого из них не существует. KISS говорит: напиши .map(normalize). YAGNI говорит: не строй мешок опций под нужды, которые ты ещё не можешь назвать. Гибкость — это догадка, и каждая ветка — это код для чтения и место для багов, пока он простаивает без дела.

4

Режим отказа: DRY-инг случайного дублирования рождает неправильную абстракцию — общий хелпер, обрастающий флагом на каждого вызывающего. Это самый дорогой из трёх отказов, потому что выглядит как хорошая работа. Ты видишь два похожих блока, выносишь хелпер shared(), чувствуешь себя добродетельным. Затем вызывающему A нужно чуть другое поведение — ты добавляешь булев. Вызывающему B нужна ещё одна правка — ты добавляешь второй булев. Каждый вызывающий передаёт свою комбинацию флагов; тело хелпера превращается в чащу if (mode === ...). Теперь у тебя нечто хуже, чем дублирование, которое ты убрал: одна функция связывает несвязанных вызывающих, и изменение ради вызывающего A рискует сломать вызывающего B.

// The wrong abstraction, fully grown:
function renderRow(item: Item, opts: {
  asAdmin?: boolean; compact?: boolean; forEmail?: boolean; legacyV1?: boolean;
}): string {
  // every combination of flags is a different caller's real requirement,
  // tangled into one body. Changing the email path can break the admin path.
}

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

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

Ревьюер говорит «заDRY-инь это». Реши, прав ли он. Две конечные точки валидируют входящую сумму:

// payments.ts
function validatePaymentAmount(cents: number): boolean {
  return Number.isInteger(cents) && cents > 0 && cents <= 1_000_000;
}

// refunds.ts
function validateRefundAmount(cents: number): boolean {
  return Number.isInteger(cents) && cents > 0 && cents <= 1_000_000;
}

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

// What the premature extraction becomes after one real requirement:
function validateAmount(cents: number, kind: "payment" | "refund"): boolean {
  const base = Number.isInteger(cents) && cents > 0 && cents <= 1_000_000;
  if (kind === "refund") return base; // ...and soon: && cents <= originalCharge
  return base;                        // ...and soon: && cents <= merchantCeiling
}

Флаг kind — это улика: один хелпер теперь обслуживает два несвязанных правила, и каждое будущее изменение одного рискует затронуть другое. Правильное решение здесь — оставить их продублированными: совпадение тел — это совпадение, и побеждают KISS/YAGNI. Сравни это со случаем НДС из Шага 2, где 1.2 и есть один факт: там ревьюер прав, и побеждает DRY. Тот же поверхностный симптом («дублированный код»), противоположные верные решения — потому что решающий вопрос — это причина для изменения, а не внешний вид.

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

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

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

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

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

У двух функций сегодня байт-идентичные тела. Ревьюер говорит «нарушение DRY, вынеси общий хелпер». Какой единственный вопрос лучше всего определяет, прав ли он?

Итог

DRY, KISS и YAGNI — эвристики, которые тянут в разные стороны, и суждение состоит в том, чтобы решить, какая доминирует здесь. DRY охраняет единый авторитетный источник для куска знания — а не для одинакового на вид текста, — и его операционный тест: «меняются ли они по одной причине?» KISS (простейшее, что работает) и YAGNI (не строй наперёд реальной нужды) — противовесы, не дающие DRY связать независимые вещи и обобщать под вызывающих, которых не существует. Сигнатурный отказ — заDRY-ить случайное дублирование в общий хелпер, который затем обрастает булевым флагом на каждого вызывающего: неправильная абстракция, которая дороже дублирования, что она заменила; выход — предпочесть дублирование и заинлайнить обратно. Ни одна из них не закон: 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.