Средний

SOLID: пять принципов, которые реально работают

Урок 1 из 6 в курсе Архитектурные паттерны и подходы к проектированию

Содержание курса (1/6)

SOLID: пять принципов, которые реально работают

SOLID — пять принципов проектирования классов, которые делают код гибким и устойчивым к изменениям. Не абстрактная теория, а конкретные правила, которые спасают при росте кодовой базы.

Почему это важно: Код без SOLID превращается в «большой комок грязи»: изменение одного модуля ломает три других, тесты невозможны без поднятия всей системы, а новые фичи требуют переписывания существующего кода. SOLID — прививка от этого.

Главная идея

Каждый принцип SOLID решает конкретную проблему: S — класс делает слишком много, O — при добавлении фичи нужно менять существующий код, L — подклассы ломают контракт родителя, I — клиент зависит от методов, которые не использует, D — высокоуровневая логика привязана к деталям реализации.

Как это выглядит на практике

  1. Заказчик просит добавить новый способ оплаты (криптовалюта)
  2. Без SOLID: открываем OrderService, добавляем if/elsif ветку — класс растёт, тесты ломаются
  3. С SRP: PaymentProcessor — отдельный класс, OrderService не знает о способах оплаты
  4. С OCP: CryptoPayment implements PaymentStrategy — ни одна строка существующего кода не меняется
  5. С DIP: OrderService зависит от интерфейса PaymentStrategy, а не от конкретного CryptoPayment
  6. Результат: новый способ оплаты = один новый файл, ноль изменений в существующем коде

Примеры кода

SRP — Single Responsibility Principle

# Плохо: один класс делает всё
class Order
  def process
    validate_items
    calculate_total
    charge_payment     # Ответственность: оплата
    send_confirmation  # Ответственность: уведомления
    update_inventory   # Ответственность: склад
    generate_invoice   # Ответственность: бухгалтерия
  end
end

# Хорошо: каждый класс — одна причина для изменения
class OrderProcessor
  def initialize(payment:, notifier:, inventory:, invoicing:)
    @payment = payment
    @notifier = notifier
    @inventory = inventory
    @invoicing = invoicing
  end

  def call(order)
    @payment.charge(order)
    @inventory.reserve(order.items)
    @invoicing.generate(order)
    @notifier.confirm(order)
  end
end

# Изменилась логика инвойсов? Меняем только InvoiceService.
# OrderProcessor не затронут.

OCP + DIP — расширение без изменения

# Open/Closed: открыт для расширения, закрыт для модификации
# Dependency Inversion: зависим от абстракции, не от реализации

# Интерфейс (в Ruby — duck typing)
class PaymentStrategy
  def charge(amount) = raise NotImplementedError
  def refund(transaction_id) = raise NotImplementedError
end

class StripePayment < PaymentStrategy
  def charge(amount)
    Stripe::Charge.create(amount: amount, currency: 'usd')
  end

  def refund(transaction_id)
    Stripe::Refund.create(charge: transaction_id)
  end
end

class CryptoPayment < PaymentStrategy
  def charge(amount)
    CoinbaseCommerce::Charge.create(amount: amount)
  end

  def refund(transaction_id)
    # Крипто-возврат через отдельную транзакцию
    CoinbaseCommerce::Refund.create(charge: transaction_id)
  end
end

# OrderService не знает, чем платят — зависит от абстракции
class OrderService
  def initialize(payment_strategy:)
    @payment = payment_strategy
  end

  def checkout(order)
    @payment.charge(order.total)  # Работает с любой стратегией
  end
end

# Добавить Apple Pay = создать ApplePayPayment < PaymentStrategy
# Ноль изменений в OrderService

Что происходит под капотом

  • SRP: «одна причина для изменения» — если требования к оплате и к уведомлениям меняются разными людьми, это разные ответственности
  • OCP: новое поведение добавляется через новые классы, а не через правку if/elsif в существующих
  • LSP: подкласс должен работать везде, где работает родитель — CryptoPayment должен реализовать и charge, и refund
  • ISP: лучше три маленьких интерфейса (Chargeable, Refundable, Subscribable), чем один огромный PaymentProvider
  • DIP: OrderService зависит от PaymentStrategy (абстракция), а не от StripePayment (деталь)

Типичные ошибки и заблуждения

  • «SOLID = много классов ради классов» — SOLID оправдан, когда система растёт и меняется; для одноразового скрипта это overkill
  • «SRP = один метод на класс» — нет, SRP = одна причина для изменения. Класс может иметь 10 методов, если все связаны с одной ответственностью
  • «Нужно применять все 5 принципов сразу» — применяйте по мере столкновения с проблемами, которые они решают

Ключевые выводы

  • SRP: один класс — одна причина для изменения. Если класс нужно менять при изменении и оплаты, и уведомлений — это нарушение
  • OCP + DIP: зависимость от абстракций позволяет добавлять новое поведение без изменения существующего кода
  • SOLID — не догма, а инструмент: применяйте там, где это снижает стоимость изменений

Термины урока

{:term=>"SRP", :definition=>"Single Responsibility Principle — класс должен иметь только одну причину для изменения"}
{:term=>"OCP", :definition=>"Open/Closed Principle — открыт для расширения (новые классы), закрыт для модификации (существующий код не меняется)"}
{:term=>"DIP", :definition=>"Dependency Inversion Principle — высокоуровневые модули зависят от абстракций, не от конкретных реализаций"}

Связь с работой backend-разработчика

SOLID — это не про классы ради классов, а про стоимость изменений. Когда заказчик говорит «добавь новый способ оплаты», ваша задача — сделать это одним новым файлом, а не правкой десяти существующих.

Мини-разбор реальной ситуации

Маркетплейс: класс OrderProcessor содержал 800 строк — оплата, инвентарь, уведомления, аналитика, логирование. Добавление нового платёжного провайдера занимало 3 дня и ломало тесты инвентаря. После декомпозиции по SRP (5 классов по 80–150 строк) добавление нового провайдера стало занимать 2 часа и не затрагивало ни одного теста за пределами платежей.

Что запомнить

  • SRP: если описание класса содержит «и» — вероятно, нарушение (OrderAndPaymentAndNotification)
  • OCP + DIP: новая фича = новый класс, ноль изменений в существующем коде
  • Применяйте SOLID при боли: класс слишком большой, изменение ломает несвязанное, сложно тестировать

Итог

SOLID — пять принципов, которые превращают monolithic god-class в набор маленьких, независимых модулей. Каждый принцип решает конкретную проблему — применяйте их, когда сталкиваетесь с этой проблемой, а не заранее.

Комментарии к уроку

Войдите, чтобы оставить комментарий.

Пока нет комментариев — будьте первым.