📐 Раздел 18 · Вопрос #1

Что такое принцип Single Responsibility и как его применять

В швейцарском армейском ноже есть нож, пила, штопор и ножницы. Он удобен в походе, но на профессиональной кухне шеф-повар режет мясо шеф-ножом, а не складным карманным ножиком....


🟢 Junior Level

Принцип единственной ответственности (SRP — Single Responsibility Principle) — это первая буква в акрониме SOLID, сформулированная Робертом Мартином (Uncle Bob):

«У класса должна быть только одна причина для изменения (A class should have only one reason to change).»

Нарушение SRP (Кухонный комбайн):
┌───────────────────────────────────────────────┐
│                   Employee                    │
│ ├── calculateSalary()   (Бухгалтерия)         │
│ ├── saveToDatabase()    (Архитекторы БД)      │
│ └── generateReport()    (Отдел кадров / HR)   │
└───────────────────────────────────────────────┘
  (3 разных отдела меняют один и тот же файл!)

Соблюдение SRP (Разделение ответственностей):
┌──────────────┐   ┌──────────────────┐   ┌────────────────────┐   ┌──────────────────┐
│   Employee   │   │ SalaryCalculator │   │ EmployeeRepository │   │ EmployeeReporter │
│ (Только поля │   │ (Логика расчета) │   │ (Работа с БД)      │   │ (Генерация PDF)  │
│  сотрудника) │   │                  │   │                    │   │                  │
└──────────────┘   └──────────────────┘   └────────────────────┘   └──────────────────┘

Простая аналогия:

В швейцарском армейском ноже есть нож, пила, штопор и ножницы. Он удобен в походе, но на профессиональной кухне шеф-повар режет мясо шеф-ножом, а не складным карманным ножиком. Если в карманном ноже сломается штопор, вам придется сдать в ремонт весь инструмент целиком, оставшись без ножа и ножниц.

Главное заблуждение:

  • ❌ «Класс должен делать ровно одно действие (иметь один метод)».
  • ✅ «Класс должен отвечать за одну бизнес-область и перед одним стейкхолдером». Класс может иметь 10 методов, но все они должны быть направлены на решение одной сфокусированной задачи (например, Money содержит методы add(), subtract(), multiply(), isZero()).

🟡 Middle Level

1. Как выявить нарушение SRP

В реальных проектах на Spring Boot нарушение SRP встречается сплошь и рядом. Главные маркеры:

  1. Правило союза «И» (“And” Rule): Если назначение класса описывается словами: «Этот сервис сохраняет заказ И списывает деньги И отправляет Email И пишет в аналитику» — SRP грубо нарушен.
  2. Множественные стейкхолдеры (Акторное правило):
    • Изменение формулы НДС требует правок в OrderService (заказчик — бухгалтерия).
    • Переход с PostgreSQL на MongoDB требует правок в том же OrderService (заказчик — DevOps).
    • Добавление пуш-уведомлений меняет тот же OrderService (заказчик — маркетинг).
  3. «Раздутый» конструктор (Constructor Bloat): Класс сервиса инжектит через @Autowired более 7–10 внешних зависимостей.
  4. Сложность Unit-тестирования: Для проверки одного тривиального метода приходится писать 50 строк настройки моков (when(...).thenReturn(...)) для 8 сторонних репозиториев.

2. Рефакторинг: от «Божественного сервиса» к SRP

// ДО: Нарушение SRP — OrderService делает всё подряд
@Service
public class OrderService {
    public void processOrder(Order order) {
        // 1. Валидация
        if (order.getTotal().compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException();
        // 2. Списание денег
        paymentGateway.charge(order.getUserId(), order.getTotal());
        // 3. Сохранение в БД
        orderRepository.save(order);
        // 4. Отправка email
        emailClient.send(order.getEmail(), "Заказ оформлен!");
        // 5. Логирование метрик
        meterRegistry.counter("orders.success").increment();
    }
}
// ПОСЛЕ: Оркестратор делегирует задачи специализированным компонентам
@Service
@RequiredArgsConstructor
public class OrderProcessingCoordinator {

    private final OrderValidator validator;
    private final PaymentProcessor paymentProcessor;
    private final OrderRepository repository;
    private final OrderNotificationService notifier;

    @Transactional
    public void processOrder(Order order) {
        validator.validate(order);
        paymentProcessor.charge(order);
        repository.save(order);
        notifier.notifyCustomer(order);
    }
}

🔴 Senior Level

1. Связность (Cohesion) и Зацепление (Coupling)

SRP является главным инструментом достижения High Cohesion (высокой внутренней связности) и Low Coupling (слабого внешнего зацепления):

Low Cohesion (Антипаттерн):
┌───────────────────────────────┐
│         AppManager            │
│ [calculateTax]  [sendSms]     │  (Методы не работают вместе, не трогают общие поля)
│ [parseXml]      [resizeImage] │
└───────────────────────────────┘

High Cohesion (Идеал):
┌───────────────────────────────┐
│        TaxCalculator          │
│ - countryRate;                │  (Все методы оперируют одними и теми же полями
│ ├── calculateVat()            │   для достижения единой цели)
│ ├── applyExemption()          │
│ └── getTotalWithTax()         │
└───────────────────────────────┘

2. Математическая метрика связности: LCOM (Lack of Cohesion in Methods)

В статическом анализе кода (SonarQube) качество SRP измеряется метриками семейства LCOM:

  • Пусть у класса есть $M$ методов и $F$ полей.
  • Рассматриваются все неупорядоченные пары методов $(M_i, M_j)$.
  • $P$ — число пар методов, которые не имеют общих полей.
  • $Q$ — число пар методов, которые обращаются хотя бы к одному общему полю. \(\text{LCOM1} = \max(0, P - Q)\) Если $\text{LCOM} > 0$, класс распадается на слабосвязанные подмножества методов и полей. Это математический сигнал к тому, что класс необходимо разделить на несколько независимых сущностей.

3. Conway’s Law (Закон Конвея) и акторы SRP

Оригинальное определение Роберта Мартина:

«Модуль должен отвечать перед одним и только одним актором (A module should be responsible to one, and only one, actor).»

Актор — это группа людей (пользователи, финансовый отдел, маркетинг), объединенная общими требованиями к поведению системы. Связь с законом Конвея: структура программного кода повторяет структуру коммуникаций в организации. Если программист объединяет в один класс расчет премии топ-менеджеров и логику экспорта табелей рабочего времени для HR, слияние веток в Git гарантированно приведет к Merge Conflicts между командами и багам в начислении зарплаты.


4 Tricky Questions

1. Как отличить класс-оркестратор (Coordinator / Facade), вызывающий 10 других сервисов, от нарушения SRP (God Object)?

Ответ: Оркестратор (например, OrderProcessingCoordinator или Saga Orchestrator) не нарушает SRP, если соблюдены три условия:

  1. Единственная ответственность оркестратора — управление бизнес-сценарием (Workflow): определение порядка шагов, обработка альтернативных путей и компенсаций.
  2. Отсутствие деталей реализации: Оркестратор не содержит низкоуровневого кода (SQL-запросов, форматирования строк, парсинга JSON, HTTP-вызовов). Он только делегирует вызовы.
  3. Единственный стейкхолдер: Сценарий меняется только тогда, когда владелец бизнес-процесса (Product Owner) меняет логику чекаута.

2. Что такое «Class Explosion» (Взрыв классов / Атомарная пыль) и когда чрезмерное следование SRP становится вредным?

Ответ: Class Explosion возникает, когда разработчики декомпозируют систему до абсурда: создают OrderValidator, OrderTotalCalculator, OrderPriceRounder, OrderDiscountApplier по 3 строчки в каждом файле. Последствия:

  • Рост когнитивной нагрузки: чтобы понять элементарную операцию, разработчик вынужден открывать 15 вкладок в IDE.
  • Раздувание Metaspace JVM из-за тысяч маленьких классов.
  • Размазывание доменной модели в Anemic Domain Model. Баланс: Объединяйте тесно связанные операции внутри богатой доменной модели (Rich Domain Model), группируя классы по бизнес-фичам (Package-by-Feature), а не по техническим микродействиям.

3. Почему популярная трехслойная архитектура (Controller - Service - Repository) часто провоцирует нарушение SRP?

Ответ: В классическом подходе создается гигантский UserService, куда разработчики складывают абсолютно все методы, где фигурирует слово User: регистрация, смена пароля, загрузка аватара, блокировка, экспорт в Excel, отправка промо-кодов. Слой @Service не задает границ ответственности. В результате UserService превращается в классический God Object на 3000 строк. Решение: Использование CQRS или вертикальной нарезки (Vertical Slice Architecture): разделение сервиса на отдельные Use Cases / Handlers: RegisterUserHandler, ChangePasswordHandler, ExportUsersToExcelHandler.

4. Нарушает ли шаблон проектирования Active Record (например, в Rails или Quarkus Panache) принцип Single Responsibility?

Ответ: Да, архитектурно нарушает. В паттерне Active Record сущность одновременно совмещает две ответственности:

  1. Бизнес-логику и данные предметной области (Domain Model).
  2. Операции сохранения, обновления и выполнения SQL-запросов к БД (Persistence Logic: user.save(), User.find()). При изменении схемы СУБД меняется доменная сущность. Однако в небольших проектах этим компромиссом осознанно жертвуют ради скорости разработки (Rapid Application Development). В крупном Enterprise стандартом является разделение на чистую сущность и Repository (паттерн Data Mapper / Spring Data JPA).

🎯 Шпаргалка для интервью

Главное:

  • SRP: У класса должна быть только одна причина для изменения (один стейкхолдер / актор).
  • SRP — это не про «один метод», а про сфокусированность бизнес-ответственности.
  • Стремитесь к High Cohesion (методы работают над общими полями) и Low Coupling (минимум зависимостей).
  • Математический маркер нарушения SRP — высокий показатель LCOM.
  • Признак нарушения в Spring Boot: класс @Service с 10+ автовайрингами зависимостей и методами для разных отделов бизнеса.
  • Альтернатива раздутым сервисам — Vertical Slice Architecture / Use Case Handlers.
  • Избегайте крайностей: чрезмерная декомпозиция приводит к антипаттерну «Атомарная пыль» (Class Explosion).

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «SRP означает, что класс должен содержать только один публичный метод» (Грубая ошибка: класс может иметь 10 методов, реализующих одну ответственность).
  • ❌ «Создание классов CommonUtils, Helper, Manager — это хорошая практика SRP» (Это свалка несогласованной логики с худшим типом Coincidental Cohesion).
  • ❌ «Я всегда дроблю код до классов в 10 строк без исключений» (Порождает атомарную пыль и раздувает навигацию).

Связанные темы: