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

Два обработчика заказа повторяют проверку суммы и адреса — кажется, пора вынести её в общую функцию. Но если через месяц один обработчик должен принимать частичный адрес, а другой — отклонять заказ без полного, общая функция быстро обрастёт флагами и условиями. В практике DRY важен не сам факт повторения строк, а то, дублируете ли вы одно и то же знание и придётся ли менять его в нескольких местах.
Сначала найдите, что именно повторяется
DRY расшифровывают как Don’t Repeat Yourself — «не повторяйся». В книге The Pragmatic Programmer принцип относится к дублированию знаний и намерений, а не к совпадающим символам. Два похожих алгоритма могут выражать разные правила; одна и та же бизнес-политика может быть продублирована в коде, документации и конфигурации, даже если эти места выглядят совершенно по-разному.
Перед рефакторингом спросите: если правило изменится, должны ли эти места измениться одновременно? Если да — вероятно, у вас дублирование знания. Если нет, сходство может быть случайным. Например, две проверки формата строки сегодня выглядят одинаково, но одна описывает идентификатор клиента, а другая — код кампании. Их требования могут разойтись независимо.
Хорошая абстракция объединяет код потому, что у него общий смысл. Совпавшие строки — только повод проверить, есть ли этот смысл.
Три приёма: от повторения к абстракции
В примерах используется Python 3.12, но критерии применимы и к другим языкам. Сравним три ситуации: оставить повторение, вынести общее правило или сделать общую функцию настраиваемой.
1. Оставьте повторение, пока различия не ясны
Два похожих фрагмента можно временно оставить рядом, если их требования ещё меняются или смысл различается. Это не отказ от качества: явный код часто дешевле, чем абстракция, которую команда пока не понимает. Добавьте короткий комментарий с причиной, если сходство наверняка подтолкнёт следующего разработчика к «универсальному» рефакторингу.
def validate_shipping_address(address: dict[str, str]) -> bool:
return bool(address.get("street") and address.get("city"))
def validate_billing_address(address: dict[str, str]) -> bool:
return bool(address.get("street") and address.get("city"))
Такое повторение оправдано, если требования к адресам доставки и оплаты различаются или вот-вот разойдутся. Цена — при общем изменении проверки придётся проверить оба места. Переименовывать переменные и выносить функцию только ради устранения двух одинаковых строк смысла нет.
2. Вынесите одно стабильное правило
Если оба обработчика действительно должны проверять одно и то же условие и меняться вместе, назовите правило по смыслу. Важно, чтобы функция выражала предметное понятие, а не только техническую операцию вроде «проверить словарь».
def has_required_address_fields(address: dict[str, str]) -> bool:
return bool(address.get("street") and address.get("city"))
def can_ship(address: dict[str, str]) -> bool:
return has_required_address_fields(address)
def can_bill(address: dict[str, str]) -> bool:
return has_required_address_fields(address)
Цена такого решения — зависимость двух сценариев от одного правила. Если требования разойдутся, функцию нужно будет разделить, а не добавлять в неё неочевидные исключения. Полезная проверка при ревью: можете ли вы объяснить назначение общей функции одним предложением без слов «универсальная» и «для всего»?
3. Не превращайте различия в россыпь флагов
Когда похожие операции отличаются одной деталью, соблазнительно создать функцию с параметрами strict, allow_empty и for_shipping. С каждым флагом растёт число сочетаний поведения, а вызывающему коду становится труднее понять, что именно происходит. Если правил действительно несколько, предпочтительнее отдельные именованные функции или явная передача простой политики, которую легко проверить.
def validate_address(
address: dict[str, str],
required_fields: tuple[str, ...],
) -> bool:
return all(address.get(field) for field in required_fields)
def can_ship(address: dict[str, str]) -> bool:
return validate_address(address, ("street", "city"))
def can_bill(address: dict[str, str]) -> bool:
return validate_address(address, ("street", "city", "postal_code"))
Такой вариант уместен, если различие сводится к списку обязательных полей и действительно является частью одного понятия — проверки адреса. Если правила оплаты и доставки скоро включат разные условия, общая функция станет точкой сцепления: тогда оставьте их отдельными.
Сравнение: что вы покупаете и чем платите
| Подход | Что выигрываете | Цена | Когда выбирать |
|---|---|---|---|
| Оставить похожий код | Независимое развитие сценариев, минимум абстракций | Общие изменения нужно внести и проверить в нескольких местах | Требования различаются или пока не устоялись |
| Вынести стабильное правило | Одно место для общего бизнес-условия | Сценарии зависят от общей реализации | Правило и причины его изменения совпадают |
| Общая функция с параметрами | Повторно используется алгоритм, варианты заданы явно | Параметры усложняют вызов и тестирование сочетаний | Различается конфигурация одного и того же понятия |
Как принять решение на практике
Разбирайте не количество совпавших строк, а связь между изменениями. Для этого пригодится короткая последовательность действий:
- Назовите правило, которое, по вашему мнению, повторяется. Если получается назвать только фрагмент реализации, например «проверка двух ключей словаря», сходство ещё не доказывает общего смысла.
- Найдите причины, по которым каждое место менялось раньше или может измениться дальше. Если причины разные, не связывайте код без необходимости.
- Сравните поведение на граничных случаях: пустое значение, отсутствующее поле, неполные данные. Совпадение обычного пути не означает совпадения требований.
- Вынесите минимальную абстракцию с понятным названием и обновите места использования. Проверьте тестами не только общий случай, но и различия между сценариями.
- Посмотрите на новый вызов со стороны читателя: понятно ли без перехода к реализации, какая политика применяется? Если нет, упростите интерфейс или верните отдельные функции.
Не пытайтесь заранее собрать все возможные варианты в один «движок». Сначала нужна реальная общая потребность, а не предположение, что похожие куски кода обязательно станут одинаковыми.
Что ломается и как заметить это на ревью
Чрезмерная абстракция обычно выдаёт себя не числом строк, а неудобством изменения. Чтобы поправить одно правило, нужно менять флаг и проверять неожиданный сценарий. Новый вариант поведения добавляют условием в общий метод, хотя он касается только одного вызывающего кода. Или общий код начинает принимать параметры, которые всегда одинаковы в каждом месте вызова.
На ревью полезны конкретные вопросы:
- «Какие места должны измениться вместе, если это правило поменяется?»
- «Эти параметры описывают варианты одного понятия или разные бизнес-процессы?»
- «Можно ли назвать функцию по предметному смыслу, а не по набору операций?»
- «Какие тесты доказывают, что сценарии действительно имеют одинаковые требования?»
Смотрите и на цену поддержки: сколько вызовов придётся обновить при изменении правила, сколько ветвей поведения нужно протестировать, сколько файлов приходится открыть, чтобы понять сценарий. Универсального числа, после которого повторение нужно устранять, нет: объём и риск зависят от кода и команды. Зато эти вопросы помогают обсуждать конкретные последствия, а не спорить о том, сколько раз можно написать одинаковую строку.
Следующий шаг: проверьте абстракцию при следующем изменении
Возьмите один недавно изменённый участок кода и проследите, какие места пришлось править вместе. Если изменения регулярно касаются одной и той же политики, вынесите её в именованную абстракцию и закрепите тестами. Если похожие фрагменты меняются независимо, оставьте их разными. После этого оцените не сокращение строк, а то, стало ли проще предсказать последствия следующей правки.
Термин: случайная дубликация — внешне одинаковый код, который обслуживает разные правила и поэтому может развиваться независимо. Она отличается от дублирования знания, когда одно требование закодировано в нескольких местах.
Защитник прав потребителей: Досудебные претензии на возврат товара со ссылками на ЗоЗПП.
Спросить за 15 ₽Источники: Martin Fowler: Once and Only Once, The Pragmatic Programmer: Don’t Repeat Yourself
Частые вопросы
Сколько раз должен повториться код, прежде чем его выносить?
Нужно ли исправлять любое повторение на ревью?
Что делать, если общая функция обросла флагами?
Подход из примеров зависит от версии Python?
А если после рефакторинга тесты проходят, но стало сложнее читать?
Какие риски у общей функции в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.