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

Приведи пример использования принципа SRP

1.6 Junior🔥 211 комментариев
#Python Core

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

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

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

Практический пример Single Responsibility Principle

SRP гласит: класс должен иметь одну причину для изменения. Давайте разберёмся на реальном примере.

Пример 1: Обработка заказов (антипаттерн)

# Плохо: класс делает ВСЁ
class Order:
    def __init__(self, items, customer):
        self.items = items
        self.customer = customer
        self.status = "pending"
    
    def calculate_total(self):
        """Расчёт суммы"""
        return sum(item.price for item in self.items)
    
    def apply_discount(self, percent):
        """Применение скидки"""
        total = self.calculate_total()
        return total * (1 - percent / 100)
    
    def send_confirmation_email(self):
        """Отправка письма"""
        import smtplib
        email = smtplib.SMTP("smtp.example.com")
        email.send_message(f"Order confirmed for {self.customer.email}")
    
    def save_to_database(self):
        """Сохранение в БД"""
        import sqlite3
        conn = sqlite3.connect("orders.db")
        cursor = conn.cursor()
        cursor.execute(f"INSERT INTO orders VALUES (...)")
        conn.commit()
    
    def generate_invoice_pdf(self):
        """Генерация счёта"""
        from reportlab.pdfgen import canvas
        c = canvas.Canvas("invoice.pdf")
        # логика генерации
        c.save()

Проблемы:

  • Класс отвечает за слишком многое
  • Если изменить формат письма → меняем Order
  • Если изменить БД → меняем Order
  • Если изменить генерацию PDF → меняем Order
  • Тестировать сложно (нужны mock для всего)
  • Переиспользование затруднено

Решение: разбить на специализированные классы

# Хорошо: каждый класс отвечает за одно

# 1. Класс для работы с заказом (только бизнес-логика)
class Order:
    def __init__(self, items, customer):
        self.items = items
        self.customer = customer
        self.status = "pending"
        self.discount_percent = 0
    
    def calculate_total(self):
        """ТОЛЬКО расчёт"""
        total = sum(item.price for item in self.items)
        return total * (1 - self.discount_percent / 100)
    
    def apply_discount(self, percent):
        """ТОЛЬКО применение скидки"""
        self.discount_percent = percent


# 2. Отдельный класс для отправки уведомлений
class OrderNotifier:
    """Отвечает ТОЛЬКО за уведомления"""
    
    def __init__(self, email_service):
        self.email_service = email_service
    
    def notify_order_confirmed(self, order):
        """Отправка письма подтверждения"""
        self.email_service.send(
            to=order.customer.email,
            subject="Заказ подтвержден",
            body=f"Ваш заказ на сумму {order.calculate_total()} рублей подтвержден"
        )


# 3. Отдельный класс для работы с БД
class OrderRepository:
    """Отвечает ТОЛЬКО за сохранение/загрузку"""
    
    def __init__(self, database):
        self.database = database
    
    def save(self, order):
        """Сохранить заказ"""
        self.database.execute(
            "INSERT INTO orders (customer_id, total) VALUES (?, ?)",
            (order.customer.id, order.calculate_total())
        )


# 4. Отдельный класс для генерации отчётов
class InvoiceGenerator:
    """Отвечает ТОЛЬКО за генерацию счётов"""
    
    def generate_pdf(self, order):
        """Создать PDF счёт"""
        from reportlab.pdfgen import canvas
        c = canvas.Canvas("invoice.pdf")
        c.drawString(100, 100, f"Invoice for {order.customer.name}")
        c.drawString(100, 80, f"Total: {order.calculate_total()}")
        c.save()
        return "invoice.pdf"


# 5. Главный орхестратор (бизнес-процесс)
class OrderProcessor:
    """Координирует процесс обработки заказа"""
    
    def __init__(self, repository, notifier, invoice_generator):
        self.repository = repository
        self.notifier = notifier
        self.invoice_generator = invoice_generator
    
    def process_order(self, order):
        """Обработать заказ (оркестрирует всё)"""
        # 1. Сохраняем
        self.repository.save(order)
        
        # 2. Уведомляем
        self.notifier.notify_order_confirmed(order)
        
        # 3. Генерируем счёт
        invoice_path = self.invoice_generator.generate_pdf(order)
        
        print(f"Заказ обработан. Счёт: {invoice_path}")

Использование:

# Инициализация
from services.email import EmailService
from db import Database

email_service = EmailService()
database = Database()

repository = OrderRepository(database)
notifier = OrderNotifier(email_service)
invoice_gen = InvoiceGenerator()

processor = OrderProcessor(repository, notifier, invoice_gen)

# Создаём и обрабатываем заказ
items = [Item("laptop", 1000), Item("mouse", 50)]
customer = Customer("john@example.com")

order = Order(items, customer)
order.apply_discount(10)

processor.process_order(order)

Преимущества этого подхода

  1. Легко менять без побочных эффектов:

    • Хочу изменить способ отправки писем? → Меняю EmailService
    • Хочу в NoSQL вместо SQL? → Меняю OrderRepository
    • Хочу JSON вместо PDF? → Меняю InvoiceGenerator
  2. Тестирование упрощается:

# Тестируем только расчёт
def test_order_calculation():
    items = [Item("item", 100)]
    order = Order(items, Customer())
    assert order.calculate_total() == 100
    # Никакие mocks не нужны!

# Тестируем уведомление отдельно
def test_notification(mock_email_service):
    notifier = OrderNotifier(mock_email_service)
    order = Order([], Customer("test@example.com"))
    
    notifier.notify_order_confirmed(order)
    
    mock_email_service.send.assert_called_once()
  1. Переиспользование:
# Хочу использовать Order в другом месте без всех зависимостей?
# Просто использую Order класс
order = Order(items, customer)
print(f"Сумма заказа: {order.calculate_total()}")
# Всё просто!
  1. Добавление новых функций не ломает старый код:
# Хочу добавить SMS уведомление?
class SMSNotifier:
    def notify_order_confirmed(self, order):
        # Отправляем SMS
        pass

# OrderProcessor не нужно менять!
processor = OrderProcessor(
    repository,
    [OrderNotifier(email), SMSNotifier()],  # Просто добавляем
    invoice_gen
)

Как я это определяю на практике

Если отвечу на вопрос: "Сколько причин у класса измениться?" — это SRP.

# Плохо
class Order:
    # Причина 1: изменится бизнес-логика расчётов
    def calculate_total(self):
        pass
    
    # Причина 2: изменится способ отправки писем
    def send_email(self):
        pass
    
    # Причина 3: изменится БД
    def save(self):
        pass

# ТРИ ПРИЧИНЫ ДЛЯ ИЗМЕНЕНИЯ!

# Хорошо
class Order:
    # ОДНА причина: изменится бизнес-логика
    def calculate_total(self):
        pass

# Другие причины в других классах
class OrderService:
    def notify(self, order): pass  # Причина: изменится уведомление

class OrderRepository:
    def save(self, order): pass   # Причина: изменится БД

Практический совет

Не переусложняй! SRP не значит, что каждый метод — отдельный класс.

# Может быть в одном классе
class OrderService:
    def __init__(self, repository, notifier):
        self.repository = repository
        self.notifier = notifier
    
    def create_and_process(self, items, customer):
        order = Order(items, customer)
        self.repository.save(order)
        self.notifier.notify(order)
        return order

# Это ОК, потому что класс отвечает за ОДИН процесс: создание и обработка заказа
# Меняется он только если меняется сам процесс

Итог

SRP = One Reason to Change

  • Разделяй ответственность
  • Каждый класс = одна ответственность
  • Легче тестировать
  • Легче переиспользовать
  • Легче менять
  • Код становится понятнее и поддерживаемее