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

Как определить, что класс имеет одну ответственность

Определение того, имеет ли класс одну ответственность (Single Responsibility Principle, SRP) — это один из ключевых практических навыков разработчика.


🟢 Junior Level

Определение того, имеет ли класс одну ответственность (Single Responsibility Principle, SRP) — это один из ключевых практических навыков разработчика.

Роберт Мартин дал точную формулировку:

«Класс имеет одну ответственность, если у него есть одна и только одна причина для изменения — то есть он отвечает перед единственным бизнес-актором.»

Простой тест «Элеваторного питча» (The Elevator Pitch Test)

Попробуйте описать назначение класса одним простым предложением:

  • Если в описании естественным образом возникают союзы «И», «ИЛИ», «А ТАКЖЕ» — у класса несколько ответственностей!
  • Плохо: «Класс OrderManager проверяет валидность заказа, И рассчитывает скидку, И сохраняет строку в БД, И отправляет чек клиенту на email».
  • Хорошо: «Класс OrderValidator проверяет корректность полей заказа».
  • Хорошо: «Класс OrderRepository сохраняет и извлекает сущности заказов из базы данных».

Наглядный пример: Определение размытой ответственности

// Слишком много обязанностей в одном классе:
public class UserService {
    // 1. Обязанность перед безопасниками: хеширование пароля
    public String hashPassword(String rawPassword) { /* ... */ }

    // 2. Обязанность перед DBA: SQL-запросы к таблице users
    public void saveUserToPostgres(User user) { /* ... */ }

    // 3. Обязанность перед отделом коммуникаций: отправка SMS
    public void sendWelcomeSms(String phone) { /* ... */ }

    // 4. Обязанность перед фронтендом: форматирование ответа в JSON
    public String mapToJson(User user) { /* ... */ }
}

Как исправить: Разнести функционал по 4 специализированным классам: PasswordEncoder, UserRepository, SmsNotificationService, UserJsonMapper.


🟡 Middle Level

5 инженерных тестов для точной диагностики SRP

В промышленной разработке Senior-инженеры используют 5 четких диагностических тестов для выявления скрытых нарушений SRP:

1. Тест бизнес-актора (The Actor Test)

Задайте вопрос: «Кто из реальных людей в компании может потребовать изменить этот файл?»

  • Если требования на доработку могут прийти одновременно от бухгалтера (формула налога), юриста (текст оферты) и DevOps-инженера (тайм-ауты соединений) — класс нарушает SRP. У класса должно быть ровно одно заинтересованное лицо (одна бизнес-роль).

2. Тест списка импортов (The Import Analysis Test)

Откройте верхнюю часть Java-файла и взгляните на список пакетов import:

import java.sql.Connection;             // Доступ к данным
import jakarta.mail.Transport;          // Почтовый протокол
import com.fasterxml.jackson.databind.*; // JSON-сериализация
import org.springframework.web.bind.*;  // HTTP/REST контроллеры

Если класс импортирует одновременно SQL-драйверы, почтовые клиенты, сетевые парсеры и аннотации контроллеров — он грубо нарушает границы архитектурных слоев и SRP.

3. Тест объема зависимостей конструктора (The Dependency Count Test)

Если конструктор класса принимает более 5–7 зависимостей:

public OrderService(UserRepo u, OrderRepo o, EmailService e, SmsService s,
                    PdfService p, TaxCalc t, DiscountCalc d, AuditLog a) // 8 зависимостей!

Это верный симптом того, что класс превратился в «божественный объект» (God Object) и стягивает на себя чужую работу. Норма для сфокусированного класса — 2–4 зависимости.

4. Тест моков в юнит-тестах (The Mocking Burden Test)

Попробуйте протестировать один метод класса. Если для проверки банального if-else вам приходится писать 30 строк настройки моков (when(mock1...).thenReturn(...); when(mock2...).thenReturn(...)), класс перегружен посторонними обязанностями.

5. Граф связности методов LCOM4 (Lack of Cohesion of Methods)

Статический анализатор (SonarQube) строит граф: узлы — методы и поля, ребра — обращения к полям.

  • Если граф распадается на 2 или более изолированные компоненты связности ($\text{LCOM4} \ge 2$), класс внутри фактически состоит из нескольких независимых программ. Его необходимо распилить на число частей, равное значению LCOM4.

🔴 Senior Level

Социально-организационный аспект: Закон Конвея

Роберт Мартин подчеркивает: SRP — это не столько про количество строк кода, сколько про организационную структуру компании (Conway’s Law):

    [Команда Биллинга]                [Команда Профиля Пользователя]
            │                                       │
            └───────────────►[User.java]◄───────────┘
                       (Точка столкновения в Git!)

Если один Java-класс регулярно редактируют разработчики из двух разных продуктовых команд (например, команда каталога и команда рекомендаций), класс нарушает SRP. Рано или поздно одна команда непреднамеренно сломает логику другой. Архитектурное решение: Разделение по ограниченным контекстам (Bounded Contexts в Domain-Driven Design) — у каждой команды появляется своя модель пользователя (BillingUser и ProfileUser), независимые друг от друга.


CQS (Command-Query Separation) как естественная граница SRP

Паттерн Бертрана Мейера Command-Query Separation (CQS) и его архитектурное развитие CQRS задают идеальную границу разделения ответственности:

  • Команды (Commands): Методы, мутирующие состояние системы (возвращают void, содержат транзакции, вызывают валидацию и запись в БД: CreateOrderCommand, CancelSubscriptionCommand).
  • Запросы (Queries): Методы, только читающие данные (идемпотентны, не имеют побочных эффектов, оптимизированы под быстрое чтение: GetOrderSummaryQuery).

Класс, совмещающий сложную бизнес-модификацию и монструозные SQL-выборки для отображения отчетов, нарушает SRP. Разделение на Command-сервисы и Query-сервисы делает кодовую базу предсказуемой и легко масштабируемой.


Изоляция сквозной функциональности (Cross-Cutting Concerns)

Частая ошибка — включение технической сквозной функциональности (логирование, сбор метрик Micrometer, распределенный трейсинг OpenTelemetry, проверка прав Spring Security) в доменные бизнес-сервисы:

// Плохо: бизнес-метод засорен посторонними обязанностями
public Order processOrder(OrderRequest req) {
    long start = System.currentTimeMillis();
    log.info("Starting order processing for user: {}", req.userId());
    securityContext.checkPermission("ORDER_CREATE");

    // Реальная бизнес-логика (3 строки)
    Order order = new Order(req);
    orderRepository.save(order);

    metrics.timer("order.process.time").record(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
    log.info("Order processed successfully: {}", order.getId());
    return order;
}

Решение по Senior-стандарту: Сквозные обязанности выносятся в Spring AOP-аспекты, HTTP Interceptors / Filters или реализуются через паттерн Decorator. Бизнес-сервис должен содержать исключительно правила предметной области.


4 Tricky Questions

1. Может ли класс из 20 строк нарушать SRP, а класс из 600 строк — идеально ему соответствовать?

Ответ: Да, безусловно. Размер класса в строках (LOC) не определяет количество его ответственностей:

  • Класс из 20 строк может содержать 3 метода: распарсить CSV, отправить REST-запрос и записать ошибку в файл — это 3 разные ответственности и 3 разных актора (нарушение SRP).
  • Класс из 600 строк может представлять собой математический парсер математических выражений (Abstract Syntax Tree Parser) или калькулятор сложного налогового законодательства. Он решает ровно одну математическую задачу, имеет одного бизнес-актора (налоговый комитет) и сплоченность $\text{LCOM4} = 1$. Дробить такой класс бессмысленно.

2. Является ли класс-оркестратор (Orchestrator / Coordinator) нарушением SRP, если он вызывает 10 разных сервисов?

Ответ: Нет, если он спроектирован правильно. У оркестратора есть своя собственная единственная ответственность — координация бизнес-процесса (Workflow Coordination). Он знает последовательность шагов алгоритма, но не знает, как именно работает каждый шаг. Если оркестратор сам не вычисляет налоги, не строит SQL-запросы и не форматирует JSON, а лишь последовательно делегирует шаги (validator.validate() -> payment.charge() -> notifier.send()), он строго соблюдает SRP.

3. Как различить «похожий код» (Accidental Duplication) и «истинное нарушение SRP» при рефакторинге?

Ответ: Это фундаментальная дилемма между принципом DRY и SRP. Если два метода в модуле регистрации пользователей и модуле оформления заказа проверяют номер телефона абсолютно одинаковым регулярным выражением:

  • Это случайное совпадение (Accidental Duplication), если правила валидации телефона для безопасности аккаунта и для службы доставки могут в будущем измениться независимо (разные акторы). Вынесение их в общий утилитный класс свяжет два независимых домена жесткой связью.
  • Истинное нарушение SRP возникает, когда код меняется по одной и той же бизнес-причине от одного актора. Только в этом случае логику следует объединять.

4. Почему паттерн «Анемичная доменная модель» (Anemic Domain Model) обманчиво выглядит как соблюдение SRP?

Ответ: Анемичная модель разделяет данные (класс-сущность только с полями и геттерами/сеттерами) и логику (сервисный класс с методами обработки). Формально кажется, что сущность идеальна по SRP: она «только хранит данные». Но на практике это разрушает инкапсуляцию ООП: сущность не может защитить свои бизнес-инварианты (любой сервис может вызвать order.setStatus(null)), а сервисы вокруг нее разрастаются в многотысячные God Objects, концентрируя в себе логику всех мыслимых сценариев. Rich Domain Model (Богатая модель в DDD) объединяет данные с операциями, защищающими инварианты, сохраняя SRP на уровне агрегата.


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

Главные критерии (30 секунд на ответ):

  • Определение Мартина: Один класс — один бизнес-актор (одна причина для изменения).
  • Быстрые тесты на SRP:
    1. Питч-тест: Описание назначения класса без союзов «И» / «А ТАКЖЕ».
    2. Тест актора: Сколько разных отделов компании могут заказать доработку файла?
    3. Тест импортов: Нет ли в одном файле java.sql, jakarta.mail и com.fasterxml.jackson?
    4. Тест моков: Нужно ли настраивать $> 4$ моков ради одного модульного теста?
    5. LCOM4: Равен ли 1 граф связности методов и полей?
  • Сквозная функциональность: Логирование, аудит и метрики выносятся в AOP/декораторы, не засоряя домен.

Чего избегать (Red Flags):

  • ❌ Оценивать SRP исключительно по числу строк кода или числу методов.
  • ❌ Считать, что классы с суффиксами Manager, Processor, Helper имеют одну ответственность (в 95% случаев это God Objects).
  • ❌ Разделять семантически неразрывный класс на 10 классов по 3 строки ради «абсолютного SRP» (риск антипаттерна Atomic Dust).

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