← Назад к вопросам

Что такое DRY?

1.0 Junior🔥 121 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое DRY

DRY (Don't Repeat Yourself) — один из фундаментальных принципов разработки программного обеспечения. Он гласит: каждый элемент знаний должен иметь единственное, непротиворечивое и авторитетное представление в системе.

Другими словами: никогда не повторяй один и тот же код, логику или данные в разных местах. Если нужна одна и та же функциональность несколько раз — выноси в отдельную функцию, класс, утилиту или константу.

Основная идея

Любой код, который появляется в нескольких местах, создаёт проблему:

# ❌ Нарушение DRY: код повторяется
def validate_email_for_user_register():
    email = input("Enter email: ")
    if "@" not in email or "." not in email:
        print("Invalid email")
        return None
    return email

def validate_email_for_user_update():
    email = input("Enter email: ")
    if "@" not in email or "." not in email:
        print("Invalid email")
        return None
    return email

def validate_email_for_newsletter():
    email = input("Enter email: ")
    if "@" not in email or "." not in email:
        print("Invalid email")
        return None
    return email

# ПРОБЛЕМЫ:
# - Если нужно изменить валидацию (например, добавить .com/.ru проверку),
#   нужно изменить в 3 местах
# - Легко забыть одно место и получить несогласованное поведение
# - Код занимает больше места
# - Сложнее поддерживать и тестировать

# ✅ Следование DRY: функция переиспользуется
def validate_email(email: str) -> bool:
    """Валидирует email адрес"""
    return "@" in email and "." in email

def register_user():
    email = input("Enter email: ")
    if not validate_email(email):
        print("Invalid email")
        return None
    return email

def update_user():
    email = input("Enter email: ")
    if not validate_email(email):
        print("Invalid email")
        return None
    return email

def subscribe_newsletter():
    email = input("Enter email: ")
    if not validate_email(email):
        print("Invalid email")
        return None
    return email

# ПРЕИМУЩЕСТВА:
# - Одно место для изменений
# - Согласованное поведение везде
# - Легче тестировать
# - Меньше кода

Уровни нарушения DRY

1. Повтор алгоритма (Algorithm Duplication)

# ❌ Один и тот же алгоритм в разных функциях
def calculate_user_fee():
    """Расчёт комиссии для пользователя"""
    fee = amount * 0.1
    if amount > 1000:
        fee = amount * 0.05
    return fee

def calculate_seller_fee():
    """Расчёт комиссии для продавца"""
    fee = amount * 0.1
    if amount > 1000:
        fee = amount * 0.05
    return fee

# Логика одинаковая! Если нужно изменить ставку — меняешь в двух местах

# ✅ Выносим логику в одну функцию
def calculate_fee(amount: float, rate: float = 0.1, threshold: float = 1000, reduced_rate: float = 0.05) -> float:
    """Расчёт комиссии"""
    return (amount * reduced_rate) if amount > threshold else (amount * rate)

user_fee = calculate_fee(amount=500)
seller_fee = calculate_fee(amount=500)

2. Повтор данных (Data Duplication)

# ❌ Одни и те же данные в разных местах
VALID_STATUSES_IN_MODULE_A = ["pending", "processing", "completed"]
VALID_STATUSES_IN_MODULE_B = ["pending", "processing", "completed"]
VALID_STATUSES_IN_MODULE_C = ["pending", "processing", "completed"]

# Если добавить новый статус, нужно обновить 3 переменные

# ✅ Одно место для данных
VALID_STATUSES = ["pending", "processing", "completed"]

# Используем везде
if status in VALID_STATUSES:
    print(f"Status {status} is valid")

3. Повтор логики валидации (Validation Logic)

# ❌ Валидация повторяется везде
class UserService:
    def register_user(self, email):
        # Валидация в методе
        if not email or "@" not in email:
            raise ValueError("Invalid email")
        # Логика
        return User(email)
    
    def update_user(self, user_id, email):
        # Та же валидация
        if not email or "@" not in email:
            raise ValueError("Invalid email")
        # Логика
        return user.update(email)

class AdminService:
    def add_user_to_group(self, email):
        # И опять валидация
        if not email or "@" not in email:
            raise ValueError("Invalid email")
        # Логика
        return group.add_user(email)

# ✅ Вываливаем валидацию в отдельный объект или декоратор
class EmailValidator:
    @staticmethod
    def validate(email: str) -> bool:
        return email and "@" in email and "." in email

def validate_email_required(func):
    def wrapper(email):
        if not EmailValidator.validate(email):
            raise ValueError("Invalid email")
        return func(email)
    return wrapper

class UserService:
    @validate_email_required
    def register_user(self, email):
        return User(email)
    
    @validate_email_required
    def update_user(self, user_id, email):
        return user.update(email)

4. Повтор SQL запросов (Query Duplication)

# ❌ SQL запросы повторяются
class UserRepository:
    def get_active_users(self):
        return db.execute(
            "SELECT * FROM users WHERE status='active' ORDER BY created_at DESC"
        )
    
    def get_users_count(self):
        return db.execute(
            "SELECT COUNT(*) FROM users WHERE status='active'"
        )
    
    def get_user_emails(self):
        return db.execute(
            "SELECT email FROM users WHERE status='active' ORDER BY created_at DESC"
        )

# Условие `status='active'` повторяется везде

# ✅ Выносим условие
class UserRepository:
    ACTIVE_USER_FILTER = "status='active'"
    
    def get_active_users(self):
        return db.execute(f"SELECT * FROM users WHERE {self.ACTIVE_USER_FILTER} ORDER BY created_at DESC")
    
    def get_users_count(self):
        return db.execute(f"SELECT COUNT(*) FROM users WHERE {self.ACTIVE_USER_FILTER}")
    
    def get_user_emails(self):
        return db.execute(f"SELECT email FROM users WHERE {self.ACTIVE_USER_FILTER} ORDER BY created_at DESC")

5. Повтор стилей и констант (Style Duplication)

# ❌ Стили повторяются в коде
class UserWidget:
    def render(self):
        return f"""
        <div style="padding: 16px; margin: 8px; background: #f5f5f5; border-radius: 8px;">
            {self.user.name}
        </div>
        """

class ProductWidget:
    def render(self):
        return f"""
        <div style="padding: 16px; margin: 8px; background: #f5f5f5; border-radius: 8px;">
            {self.product.name}
        </div>
        """

# ✅ Константы и компоненты
CARD_STYLE = "padding: 16px; margin: 8px; background: #f5f5f5; border-radius: 8px;"

class BaseCard:
    def render_card(self, content):
        return f'<div style="{CARD_STYLE}">{content}</div>'

class UserWidget(BaseCard):
    def render(self):
        return self.render_card(self.user.name)

class ProductWidget(BaseCard):
    def render(self):
        return self.render_card(self.product.name)

Практические примеры DRY

Пример 1: Функция vs Код

# ❌ Без DRY
def order_summary():
    total = 0
    for item in cart:
        total += item.price * item.quantity
    tax = total * 0.1
    return total + tax

def invoice_summary():
    total = 0
    for item in invoice_items:
        total += item.price * item.quantity
    tax = total * 0.1
    return total + tax

def monthly_report():
    total = 0
    for item in monthly_items:
        total += item.price * item.quantity
    tax = total * 0.1
    return total + tax

# ✅ С DRY
def calculate_total_with_tax(items, tax_rate=0.1):
    subtotal = sum(item.price * item.quantity for item in items)
    return subtotal * (1 + tax_rate)

order_total = calculate_total_with_tax(cart)
invoice_total = calculate_total_with_tax(invoice_items)
monthly_total = calculate_total_with_tax(monthly_items)

Пример 2: Конфигурация

# ❌ Без DRY: константы разбросаны
class EmailService:
    SMTP_HOST = "smtp.gmail.com"
    SMTP_PORT = 587
    SENDER_EMAIL = "noreply@example.com"

class BackupService:
    EMAIL_SERVER = "smtp.gmail.com"
    EMAIL_PORT = 587
    FROM_ADDRESS = "noreply@example.com"

class ReportService:
    MAIL_HOST = "smtp.gmail.com"
    MAIL_PORT = 587
    MAIL_FROM = "noreply@example.com"

# ✅ С DRY: одно место для конфигурации
class Config:
    SMTP_HOST = "smtp.gmail.com"
    SMTP_PORT = 587
    SENDER_EMAIL = "noreply@example.com"

class EmailService:
    def __init__(self, config: Config):
        self.host = config.SMTP_HOST
        self.port = config.SMTP_PORT
        self.sender = config.SENDER_EMAIL

Пример 3: Мixin для переиспользования

# ❌ Код для сохранения timestamps повторяется
class Article:
    def __init__(self):
        from datetime import datetime
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
    
    def save(self):
        from datetime import datetime
        self.updated_at = datetime.now()
        db.save(self)

class Comment:
    def __init__(self):
        from datetime import datetime
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
    
    def save(self):
        from datetime import datetime
        self.updated_at = datetime.now()
        db.save(self)

# ✅ С DRY: Mixin
from datetime import datetime

class TimestampMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.created_at = datetime.now()
        self.updated_at = datetime.now()
    
    def save(self):
        self.updated_at = datetime.now()
        super().save()

class Article(TimestampMixin):
    def save(self):
        super().save()
        # Другая логика для Article

class Comment(TimestampMixin):
    def save(self):
        super().save()
        # Другая логика для Comment

Пограничные случаи DRY

# ⚠ ОСТОРОЖНО: не переусложни с DRY

# Иногда два почти одинаковых блока кода лучше оставить как есть,
# если они решают разные задачи и могут развиваться отдельно

def validate_user_email(email):
    return "@" in email

def validate_newsletter_email(email):
    return "@" in email

# Выглядит как нарушение DRY, но:
# - validate_user_email может добавить требование к длине
# - validate_newsletter_email может требовать подтверждения
# - Они могут эволюционировать по-разному
# 
# В этом случае дублирование оправдано, чтобы не создавать
# излишние зависимости между не связанными компонентами

Инструменты для проверки DRY

# Инструменты, которые помогают найти дублирование:

# 1. Code duplication detectors
# - CPDCheck (Copy Paste Detector) — находит копипасты
# - pylint с плагинами — анализирует код

# 2. Статический анализ
# - flake8 — простая проверка
# - pylint — углублённый анализ
# - radon — метрики сложности

# 3. IDE features
# - PyCharm: Analyze -> Run Inspection by Name -> Duplicated Code
# - VSCode: с расширениями

Баланс между DRY и другими принципами

# DRY может конфликтовать с другими принципами:

# 1. DRY vs YAGNI (You Aren't Gonna Need It)
# ❌ Переусложнение для предполагаемого дублирования в будущем
def create_generic_validator(*args, **kwargs):
    # Создали сложный валидатор для "будущего дублирования"
    # Но это дублирование никогда не произойдёт!
    pass

# ✅ Создавай абстракции только для ТЕКУЩЕГО дублирования

# 2. DRY vs SOLID (Single Responsibility)
# ❌ Объединяем несвязанный код в одну функцию
def do_everything(user_id):
    # Валидирует, загружает, сохраняет, отправляет email
    # Выглядит как DRY, но нарушает SRP
    pass

# ✅ Разные ответственности = разные функции

Заключение

DRY (Don't Repeat Yourself) — это не просто избежание дублирования кода. Это принцип, который учит:

  • Искать общие паттерны и выносить их
  • Хранить каждый факт в одном месте
  • Делать код более maintainable
  • Снижать вероятность ошибок при изменениях
  • Упрощать тестирование и рефакторинг

Однако не переусложняй: создавай абстракции только для реального дублирования, а не для предполагаемого. DRY — это баланс между переиспользованием и читаемостью.