SOLID: пять принципов, которые реально работают
SOLID — пять принципов проектирования классов, которые делают код гибким и устойчивым к изменениям. Не абстрактная теория, а конкретные правила, которые спасают при росте кодовой базы.
Почему это важно: Код без SOLID превращается в «большой комок грязи»: изменение одного модуля ломает три других, тесты невозможны без поднятия всей системы, а новые фичи требуют переписывания существующего кода. SOLID — прививка от этого.
Главная идея
Каждый принцип SOLID решает конкретную проблему: S — класс делает слишком много, O — при добавлении фичи нужно менять существующий код, L — подклассы ломают контракт родителя, I — клиент зависит от методов, которые не использует, D — высокоуровневая логика привязана к деталям реализации.
Как это выглядит на практике
- Заказчик просит добавить новый способ оплаты (криптовалюта)
- Без SOLID: открываем OrderService, добавляем if/elsif ветку — класс растёт, тесты ломаются
- С SRP: PaymentProcessor — отдельный класс, OrderService не знает о способах оплаты
- С OCP: CryptoPayment implements PaymentStrategy — ни одна строка существующего кода не меняется
- С DIP: OrderService зависит от интерфейса PaymentStrategy, а не от конкретного CryptoPayment
- Результат: новый способ оплаты = один новый файл, ноль изменений в существующем коде
Примеры кода
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 — не догма, а инструмент: применяйте там, где это снижает стоимость изменений
Термины урока
Связь с работой backend-разработчика
SOLID — это не про классы ради классов, а про стоимость изменений. Когда заказчик говорит «добавь новый способ оплаты», ваша задача — сделать это одним новым файлом, а не правкой десяти существующих.
Мини-разбор реальной ситуации
Маркетплейс: класс OrderProcessor содержал 800 строк — оплата, инвентарь, уведомления, аналитика, логирование. Добавление нового платёжного провайдера занимало 3 дня и ломало тесты инвентаря. После декомпозиции по SRP (5 классов по 80–150 строк) добавление нового провайдера стало занимать 2 часа и не затрагивало ни одного теста за пределами платежей.
Что запомнить
- SRP: если описание класса содержит «и» — вероятно, нарушение (OrderAndPaymentAndNotification)
- OCP + DIP: новая фича = новый класс, ноль изменений в существующем коде
- Применяйте SOLID при боли: класс слишком большой, изменение ломает несвязанное, сложно тестировать
Итог
SOLID — пять принципов, которые превращают monolithic god-class в набор маленьких, независимых модулей. Каждый принцип решает конкретную проблему — применяйте их, когда сталкиваетесь с этой проблемой, а не заранее.
Комментарии к уроку
Войдите, чтобы оставить комментарий.
Пока нет комментариев — будьте первым.