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

Принцип DRY, «don't repeat yourself», обычно переводят как запрет на повторяющийся код. Это упрощение дорого обходится командам: разработчики начинают сращивать в одну функцию вещи, которые совпадают только внешне, и получают на выходе класс с десятком параметров и условий внутри. Чтобы DRY приносил пользу, а не вред, нужно понимать, что именно он запрещает повторять.
Знание, а не текст
В книге The Pragmatic Programmer DRY определён иначе, чем в большинстве пересказов: каждый фрагмент знания должен иметь единственное, недвусмысленное представление в системе. Знание здесь это конкретная функциональность бизнес-домена или алгоритм, а не любая визуально похожая последовательность символов.
Эта разница кажется мелкой, но от неё зависит, что команда будет делать на код-ревью. Если «не повторять» толкуют буквально, разработчики начинают искать любое визуальное сходство между участками кода и сращивать их в общую функцию, даже когда совпадение случайное. Если держать в фокусе именно знание, а не текст, вопрос меняется: несёт ли этот участок кода одно и то же бизнес-решение, которое завтра может измениться и потребовать правки сразу во всех копиях.
Возьмём пример. У интернет-магазина есть класс доставки, который считает срок прибытия заказа. Если эта логика скопирована в семидесяти местах приложения, то изменение правил доставки превращается в охоту по всей кодовой базе с риском пропустить пару мест. Вот это настоящее нарушение DRY: одно и то же знание размножено и живёт своей жизнью в каждой копии.
final class Shipment
{
public function __construct(
private readonly int $deliveryDays = 4,
) {
}
public function calculateDeliveryDate(\DateTimeImmutable $from): \DateTimeImmutable
{
return $from->modify("+{$this->deliveryDays} days");
}
}Цена такого нарушения растёт нелинейно с размером проекта. На маленьком сервисе из пяти контроллеров разработчик может держать все копии логики в голове и обновить их за один проход. На проекте с полусотней модулей и несколькими командами это становится почти невозможным: правка в одном месте проходит код-ревью и уезжает в продакшен, а соседняя копия того же правила остаётся нетронутой до первой жалобы пользователя.
Именно поэтому формулировка из The Pragmatic Programmer говорит не о коде, а о представлении знания. Если в системе есть одно официальное место, где описано правило расчёта доставки, то вопрос «а где это обновить» всегда имеет один ответ, и новый участник команды тратит время на понимание домена, а не на поиск всех копий одной и той же логики по репозиторию.
Когда одинаковый код не нарушает DRY
Теперь возьмём класс валидации CSV-импорта с тремя одинаковыми блоками if (!isset(...)) на разные поля товара. Выглядит как повтор, который нужно сократить циклом по массиву атрибутов. С точки зрения читаемости это правильная правка, но называть исходный вариант нарушением DRY некорректно: бизнес-логика проверки живёт в одном методе, просто внутри него есть лишние строки.
Разница принципиальна. Дублирование знания означает, что одна и та же бизнес-правда описана в нескольких местах кода и будет расходиться при изменениях. Дублирование синтаксиса это просто повторяющиеся конструкции внутри одного места принятия решения, и их можно сократить циклом, мапой или паттерном без всякого отношения к DRY.
| Признак | Дублирование синтаксиса | Дублирование знания |
|---|---|---|
| Где живёт правка | В одном методе или файле | Разбросана по модулям |
| Риск при изменении | Потерять читаемость | Забыть обновить копию и получить баг |
| Лечится | Циклом, константой, вспомогательной функцией | Единым сервисом или классом домена |
Совпадение не значит родство
Опаснее другая ситуация: два класса с одинаковым числом внутри выглядят как повод для объединения, хотя представляют разные понятия домена. Срок доставки заказа и срок возврата товара могут оба равняться четырём дням просто по совпадению бизнес-правил на данный момент.
Проверка простая: нужно спросить, изменятся ли эти два значения одновременно по одной и той же бизнес-причине, или у каждого своя причина для изменения. Если отдел логистики меняет сроки доставки независимо от отдела поддержки, который отвечает за политику возврата, перед нами два разных знания, а не одно, случайно совпавшее по цифре.
final class Shipment
{
public const DELIVERY_DAYS = 4;
}
final class OrderReturn
{
public const RETURN_WINDOW_DAYS = 4;
}Цена слишком ранней абстракции
Практика переноса похожих контроллеров в общий абстрактный класс ради DRY часто создаёт больше проблем, чем решает: два приложения с разными сценариями развития начинают тянуть друг друга за рукав через условные блоки внутри общего родителя. Сэнди Метц формулирует это коротко: дублирование обходится дешевле неверной абстракции. Скопированный код легко развести в разные стороны позже, а расцепить два модуля, которые уже срослись через общий интерфейс, почти всегда дороже.
- Правило трёх повторений: абстрагировать стоит, когда одинаковая логика встретилась минимум трижды, а не «на будущее»
- Если в общей функции появляются параметры типа
$isSpecialCase, это сигнал расцепить её обратно - Контроллеры и представления почти никогда не содержат знания бизнес-домена, их совпадение редко стоит обобщения
DRY шире, чем код
Знание системы не ограничено исходниками: это ещё и схема базы данных, конфигурация сборки, документация, тестовые сценарии. Если одно и то же правило продублировано в коде и в README, расхождение всё равно рано или поздно всплывёт, просто в виде путаницы у новых участников команды, а не в виде падения продакшена.
Классический пример из миграций базы данных: правило валидации длины поля описано и в коде приложения, и в ограничении столбца, и в документации API. Три источника правды почти гарантированно начнут расходиться при первом изменении, если никто не договорился, какой из них главный. Разумная практика: выбрать один источник, например ограничение в схеме, и генерировать из него остальные описания, а не держать три независимые копии одного числа.
Как действовать на практике
- Сначала написать работающий код для конкретного случая, не пытаясь угадать будущие варианты использования
- Когда одна и та же бизнес-логика реально встретилась несколько раз, выделить для неё единственное представление: метод, сервис, класс
- Регулярно проверять, не держится ли общая абстракция только на совпадении цифр или имён, без общего смысла
- Если в общем коде накопились условные ветвления под разные контексты, разделить его обратно, даже если придётся скопировать часть строк
Эти шаги работают в обе стороны: они одинаково защищают и от слепого копирования логики по всему проекту, и от обратной крайности, когда разработчик тащит в общую абстракцию всё, что визуально похоже. Решение в обоих случаях принимается не по ощущению «красиво или некрасиво», а по вопросу, какое знание домена перед нами и сколько у него реальных владельцев в бизнесе.
DRY остаётся одним из самых полезных принципов именно потому, что он не про борьбу с похожими строчками, а про то, чтобы у каждого бизнес-правила был один хозяин в кодовой базе. Как только это прочитано буквально и применяется к любому визуальному совпадению, принцип превращается в источник хрупких конструкций, которые сложнее поддерживать, чем исходное дублирование.
Частые вопросы
DRY это запрет на повторяющиеся строки кода?
Нет. DRY запрещает повторять знание: бизнес-правило или алгоритм. Похожие по виду строки, которые не несут общего смысла, можно спокойно оставить раздельными.
Когда стоит выносить общую логику в абстракцию?
Когда одна и та же бизнес-логика встретилась минимум трижды и очевидно останется неизменной в обозримое время. На первом и втором повторении обычно выгоднее подождать.
Что делать, если абстракция уже разрослась условиями под разные случаи?
Разделить её обратно на отдельные реализации, даже если это означает скопировать часть кода. Копию проще развести в разные стороны, чем расцепить срощенный общий модуль.



