Если вы занимаетесь программированием больше пяти минут, то наверняка слышали священную мантру: «Не повторяйся». Её воспринимают как священное писание о качестве кода, шепчут во время ревью кода, проповедуют на курсах и используют разработчики повсюду как некое заклинание программного обеспечения. Но вот в чём дело — и я говорю это со всей любовью к чистому коду — догматичное следование принципу DRY может быть одним из самых эффективных способов создать кошмар при поддержке кода.
Я не говорю, что DRY — это плохо. Я говорю, что это как соль: необходима в правильном количестве, но вы можете испортить блюдо, если будете относиться к ней как к универсальному решению для всего.
Принцип DRY: понимание того, о чём мы на самом деле говорим
Прежде чем мы начнём нарушать правила, давайте разберёмся в самом правиле. DRY (Don’t Repeat Yourself) стал популярным благодаря книге The Pragmatic Programmer, где он определяется так: «Каждая часть знаний должна иметь единственное, однозначное, авторитетное представление в системе».
Звучит разумно, правда? Идея в том, что если вы изменяете бизнес-правило или исправляете ошибку, вам не нужно искать по всему коду, как по карте сокровищ с 47 крестиками. Обновили один раз — и готово, исправилось везде. Принцип поощряет сокращение дублирования кода, что часто ведёт к более поддерживаемым и менее подверженным ошибкам программам.
Но вот где всё становится интереснее: повторяющийся код и повторяющиеся знания — это не одно и то же. Это различие абсолютно crucial, и именно здесь большинство разработчиков совершают ошибки.
Кошмарный сценарий: преждевременная абстракция
Позвольте мне описать вам ситуацию. Вы только начали работу над новой функцией. Есть фрагмент логики, который обрабатывает данные пользователя — валидация, трансформация и тому подобное. Вы пишете его один раз. Затем, позже в том же спринте, вам нужен похожий код в другом месте. Не идентичный, но достаточно похожий, чтобы ваши мышцы, чувствительные к DRY, дёрнулись.
Поэтому вы делаете то, чему вас научили: создаёте абстракцию. Вы извлекаете функцию. Может быть, добавляете несколько параметров, чтобы сделать её «гибкой». Вы чувствуете себя хорошо. Вы следуете лучшим практикам. Вы профессионал.
Через шесть месяцев кто-то должен немного изменить один из этих потоков. Они меняют общую функцию, и внезапно три разные функции ведут себя неожиданно. Изменение, которое имело смысл для функции А, ломает функцию В. Теперь у вас классический случай неправильного связывания — части системы, которые не должны знать друг о друге, тесно связаны между собой.
Это то, что мы называем преждевременной абстракцией, и она коварна, потому что выглядит профессионально, пока вы её делаете.
# Подход DRY (который вызывает проблемы)
def process_data(data, transform_type="standard", validate=True, normalize=False, clean=True):
"""
Эта функция делает всё. Она ГИБКАЯ!
Спойлер: она не гибкая, она беспорядок.
"""
if validate:
# Логика валидации
pass
if transform_type == "standard":
# Стандартная трансформация
pass
elif transform_type == "special":
# Специальная трансформация
pass
elif transform_type == "legacy":
# Трансформация наследия
pass
if normalize:
# Нормализация
pass
if clean:
# Очистка
pass
return data
# Теперь у вас есть функция с четырьмя булевыми флагами и тремя поведенческими путями
# Измените одно, сломайте два других. Поздравляю!
Где DRY вызывает реальные проблемы
1. Читаемость приносится в жертву на алтаре переиспользования
Вы знаете, что проще понять? Простой, прямолинейный код, который делает одну вещь понятным способом. Вы знаете, что трудно понять? Универсальная, параметризованная функция, которая пытается обработать все возможные случаи.
Иногда повторение кода — это более честный подход. Если два фрагмента кода делают похожие вещи, но имеют совершенно разные причины для изменения, дублирование их сохраняет намерения ясными:
# ПЛОХО: Чрезмерная абстракция, попытка быть умным
def calculate_total(items, discount_multiplier=1.0, apply_tax=True, round_result=False):
total = sum(item.price for item in items)
total *= discount_multiplier
if apply_tax:
total *= 1.08
return round(total) if round_result else total
# ХОРОШО: Ясно, просто и честно о том, что делает
def calculate_invoice_total(items):
"""Рассчитывает общую сумму для счёта, включая налог"""
subtotal = sum(item.price for item in items)
return round(subtotal * 1.08, 2)
def calculate_cart_subtotal_with_discount(items, discount_percentage):
"""Рассчитывает промежуточную сумму корзины с учётом скидки клиента"""
subtotal = sum(item.price for item in items)
return subtotal * (1 - discount_percentage / 100)
Да, есть дублирование. Но каждая функция имеет чёткую, единственную цель. Если бизнес-правила меняются для счетов, вы изменяете calculate_invoice_total. Вы не случайно ломаете корзину, потому что эти функции намеренно разделены.
2. Связывание: тихий убийца кода
Когда вы агрессивно преследуете DRY, вы делаете молчаливую ставку: что эти фрагменты кода должны изменяться вместе. Часто эта ставка неверна.
Рассмотрим этот пример из реального мира: у вас есть поток регистрации пользователей в веб-приложении, и позже вам нужна похожая валидация пользователя в мобильном API. Они оба проверяют адреса электронной почты, проверяют прочность пароля, гарантируют уникальность имён пользователей. Поэтому вы создаёте общий модуль валидации.
Проходят годы. Веб-приложение растёт. Требования к безопасности меняются. Вы хотите добавить многофакторную аутентификацию в веб-регистрацию. Поэтому вы изменяете общий модуль валидации. Теперь каждое приложение, использующее этот модуль, внезапно ожидает MFA, даже если мобильное API не имеет такой возможности.
Это связывание. Это боль.
# ДО: Общая валидация (кажется отличной!)
def validate_user(email, password, username):
if not is_valid_email(email):
raise ValidationError("Неверный адрес электронной почты")
if len(password) < 12: # Строгое требование добавлено для веб
raise ValidationError("Пароль слишком слабый")
if not is_unique_username(username):
raise ValidationError("Имя пользователя занято")
return True
# Это используется везде. Затем служба безопасности говорит:
# "Нам нужна MFA для веб-регистрации"
# Вы добавляете MFA в validate_user() и ломаете мобильное API
# ПОСЛЕ: Отдельная, намеренная валидация
def validate_web_registration(email, password, username, mfa_enabled=False):
if not is_valid_email(email):
raise ValidationError("Неверный адрес электронной почты")
if len(password) < 12:
raise ValidationError("Пароль слишком слабый")
if not is_unique_username(username):
raise ValidationError("Имя пользователя занято")
if mfa_enabled and not supports_mfa():
raise ValidationError("Требуется MFA")
return True
def validate_mobile_registration(email, password, username):
if not is_valid_email(email):
raise ValidationError("Неверный адрес электронной почты")
if len(password) < 8: # Другое требование
raise ValidationError("Пароль слишком слабый")
if not is_unique_username(username):
raise ValidationError("Имя пользователя занято")
return True
# Да, есть дублирование. Но теперь они могут развиваться независимо.
3. Неправильная абстракция хуже дублирования
Вот цитата, которую следует поместить в рамку и повесить в каждом офисе разработчика: «Дублирование гораздо дешевле, чем неправильная абстракция» — Сэнди Метц.
Подумайте над этим. Если вы обнаружите позже, что ваша общая функция была неправильным выбором, вам придётся распутать её. Вам нужно найти везде, где она используется. Вам нужно аккуратно извлечь логику обратно. Вы вносите ломающие изменения в несколько частей системы одновременно. Это экспоненциально дороже, чем если бы вы просто оставили дублирование на месте и нашли правильную абстракцию позже.
Лучший подход: программирование AHA
Есть философский подход, который я нахожу гораздо более практичным, чем строгий DRY. Он называется AHA Programming (избегайте поспешных абстракций), продвигаемый Кентом С. Доддсом.
Основной принцип: Не будьте догматичны в отношении того, когда вы начинаете писать абстракции. Пишите абстракцию, когда это кажется правильным, и не бойтесь дублировать код, пока не дойдёте до этого.
Вот как это работает:
Шаг 1: Напишите это один раз
Вы определяете паттерн. Вы пишете код для его решения.
Шаг 2: Напишите это снова (да, снова)
Вы сталкиваетесь с тем же паттер
