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

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

Представьте офис: бухгалтер считает зарплату, юрист проверяет договоры, системный администратор настраивает серверы. Если один человек пытается делать всё сразу — качество падает.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Единственная ответственность (SRP) означает, что класс должен делать что-то одно. Если вы можете описать, что делает класс, одним простым предложением без слов “и”, “или”, “также” — скорее всего, ответственность одна.

Представьте офис: бухгалтер считает зарплату, юрист проверяет договоры, системный администратор настраивает серверы. Если один человек пытается делать всё сразу — качество падает.

Пример нарушения SRP:

// Этот класс делает слишком много: валидирует, сохраняет и отправляет email
public class OrderProcessor {
    public void process(Order order) {
        validate(order);       // Валидация — ответственность валидатора
        save(order);           // Сохранение — ответственность репозитория
        sendEmail(order);      // Уведомление — ответственность сервиса уведомлений
        generateInvoice(order);// Генерация PDF — ответственность генератора отчётов
    }
}

Пример с соблюдением SRP:

// Каждый класс — одна ответственность
public class OrderValidator {
    public void validate(Order order) {
        if (order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order is empty");
        }
    }
}

public class OrderRepository {
    public void save(Order order) {
        // сохранение в БД
    }
}

public class EmailNotificationService {
    public void sendConfirmation(Order order) {
        // отправка email
    }
}

// Оркестратор только координирует
public class OrderService {
    private final OrderValidator validator;
    private final OrderRepository repository;
    private final EmailNotificationService notifier;

    public OrderService(OrderValidator validator,
                        OrderRepository repository,
                        EmailNotificationService notifier) {
        this.validator = validator;
        this.repository = repository;
        this.notifier = notifier;
    }

    public void process(Order order) {
        validator.validate(order);
        repository.save(order);
        notifier.sendConfirmation(order);
    }
}

Простой тест на SRP: Попробуйте описать назначение класса одним предложением. Если используете слово “и” — у класса несколько ответственностей.


🟡 Middle Level

5 тестов для определения SRP

1. Тест имени (The Naming Test)

Опишите, что делает класс, не используя слова “и”, “или”, “также”, “ещё”.

Класс Название Результат
OrderProcessor “Проверяет заказ И сохраняет И отправляет почту” ❌ 3 ответственности
OrderValidator “Проверяет корректность заказа” ✅ 1 ответственность
PdfGenerator “Генерирует PDF-документы” ✅ 1 ответственность
UserManager “Управляет пользователями” — но что именно? ❌ Размыто

2. Тест изменения (The Change Test)

Самое точное определение от Роберта Мартина. Спросите: “Кто может прийти и потребовать изменить этот класс?”

Если это и юристы (текст договора), и бухгалтеры (формула расчёта), и админы (путь к логам) — у класса минимум 3 ответственности.

3. Тест зависимостей (The Import Test)

Посмотрите на импорты класса:

import java.sql.*;          // БД
import javax.mail.*;        // Email
import com.fasterxml.jackson.*; // JSON
import org.springframework.web.*; // Web

Если в одном классе есть java.sql, javax.mail и com.fasterxml.jackson — он “размазан” между БД, сетью и сериализацией.

4. Тест моков (The Mocking Test)

Попробуйте написать unit-тест на один метод. Если для проверки одного if нужно настроить 5-7 моков — класс перегружен чужими ответственностями.

// Плохо: 7 моков для одного теста
@Mock private OrderRepository repo;
@Mock private EmailService email;
@Mock private SmsService sms;
@Mock private PdfGenerator pdf;
@Mock private TaxCalculator tax;
@Mock private DiscountService discount;
@Mock private AuditLogger audit;

// Хорошо: 1-2 мока, тестируем одну ответственность
@Mock private OrderRepository repo;
@Test void shouldSaveValidOrder() { ... }

5. Метрика связности LCOM4

LCOM4 (Lack of Cohesion in Methods) вычисляется статическими анализаторами:

  • Представьте граф, где узлы — методы и поля, рёбра — использование поля методом
  • Если граф распадается на 2+ несвязанных компонента — внутри одного класса живут независимые “тела”
LCOM4 Значение
1 ✅ Высокая связность, одна ответственность
2-3 ⚠️ Класс можно разделить
4+ ❌ Класс точно нужно разделить

Инструменты: SonarQube, jdepend, IntelliJ IDEA (встроенный анализ).

Социальный фактор SRP

Интересный взгляд: “Один класс — одна команда разработчиков”. Если над одним классом работают люди из разных команд (команда Биллинга и команда Доставки) — он станет узким местом. Разделите по границам ответственности команд.

Типичные ошибки

  1. Ошибка: “Класс маленький — значит SRP соблюдён.” Решение: Размер не определяет ответственность. Класс из 30 строк может делать 3 вещи. Класс из 300 строк может делать одну сложную вещь.

  2. Ошибка: “У класса один публичный метод — значит SRP соблюдён.” Решение: Один метод может делегировать 10 разным подсистемам. Смотрите на связность, а не на количество методов.

  3. Ошибка: Анемичная модель (только геттеры/сеттеры) — это не нарушение SRP, это перенос ответственности в сервисы. Решение: В классическом ООП логика рядом с данными. В Enterprise Java анемичная модель часто оправдана (ORM, DI).

Когда НЕ стоит строго следовать SRP

  • DTO / Value Objects: Классы только для данных — их ответственность “хранить данные”, это нормально
  • Тестовые утилиты: Test fixtures, builders — могут содержать разнородную логику
  • Маленькие утилиты: Класс на 20 строк с двумя связанными методами не нужно дробить

Сравнение подходов

Подход Плюсы Минусы Когда использовать
Строгий SRP (каждая операция — отдельный класс) Максимальная тестируемость Class explosion, сложность навигации Критичные к безопасности системы (safety-critical — авиация, медицина), где цена ошибки крайне высока
Разумный SRP (1 домен = 1 класс) Баланс Требует зрелого суждения Большинство enterprise-проектов
Без SRP Быстро на старте Неподдерживаемо через год Прототипы, одноразовые скрипты

🔴 Senior Level

Internal Implementation: Как JVM видит SRP

На уровне JVM SRP не существует — это architectural constraint. Но последствия нарушения SRP измеримы:

Memory footprint: Класс с 5 ответственностями = 5 групп полей, которые редко используются одновременно. Это означает:

  • Объект занимает 200+ байт, но в конкретном контексте используются 20 байт
  • Остальные 180 байт — “мёртвый вес” в кэше CPU (cache line pollution)
  • Для L1 cache (32-48KB на ядро) это означает меньше полезных данных на cache line

GC implications: Большой объект с множеством полей:

  • Promoted в old generation быстрее (больше размер, быстрее заполняется tenured)
  • При частичном использовании — wasted memory в old gen
  • G1 GC выделяет регионы по 1-32MB. Большой класс с множеством ссылок может “зацепить” несколько регионов

JIT compilation: Класс с 50 методами и 30 полями:

  • JIT не может эффективно оптимизировать — слишком много путей выполнения
  • Методы редко вызываются вместе — нет benefit от compilation batching
  • Code cache fragmentation — разные “ответственности” компилируются отдельно, но занимают соседние участки

Архитектурные Trade-offs

Строгий SRP (каждая операция — отдельный класс):

  • ✅ Плюсы: Изоляция изменений; минимальный blast radius; каждый класс тестируется отдельно; легко мокировать
  • ❌ Минусы: Class explosion (5000+ классов); сложность навигации; overhead на создание объектов; трудно отследить flow

Умеренный SRP (группировка по доменному контексту):

  • ✅ Плюсы: Баланс между читаемостью и изоляцией; логически связанные операции вместе
  • ❌ Минусы: Субъективные границы; возможны “ползущие” ответственности

Гибридный SRP (строгий в domain, умеренный в infrastructure):

  • ✅ Плюсы: Чистый domain model; pragmatism в инфраструктуре
  • ❌ Минусы: Неоднородность; нужно помнить где какой стиль

Edge Cases

  1. Анемичная модель (Anemic Domain Model): User и Order только с геттерами/сеттерами. Формально — не нарушение SRP (одна ответственность: хранение данных). Но вся бизнес-логика уходит в сервисы, которые становятся God Objects. Решение: В DDD используйте Rich Domain Model. В Transaction Script — примите анемичную модель, но контролируйте размер сервисов.

  2. Class Explosion: 50 классов по SRP для простой CRUD-операции. Решение: Не бойтесь создавать много классов (IDE справляются с 10,000+ файлов). Бойтесь одного файла на 10,000 строк. Но если класс из 3 методов делает одну вещь — не дробите.

  3. God Class с “хорошим” именем: PaymentOrchestrator на 5000 строк с 30 зависимостями. Имя звучит “одной ответственностью” (оркестрация), но это маскировка. Решение: Используйте метрику количества зависимостей (Dependency Count — подсчёт числа классов, от которых зависит данный класс). Если класс зависит от 10+ других классов — это God Class.

  4. Cross-cutting Concerns (сквозная функциональность — логирование, метрики, tracing, которые нужны во многих классах): Логирование, метрики, tracing — это ответственность, которая “размазана” по всем классам. Решение: Используйте аспекты (AOP — аспектно-ориентированное программирование), декораторы или middleware. Не смешивайте cross-cutting concern с бизнес-логикой в одном классе.

// Плохо: логирование — часть ответственности сервиса
public class OrderService {
    private final Logger log = LoggerFactory.getLogger(OrderService.class);
    private final MeterRegistry metrics;

    public Order process(OrderRequest req) {
        log.info("Processing order {}", req.getId());
        metrics.counter("orders.processed").increment();
        // бизнес-логика...
        log.info("Order {} processed", req.getId());
    }
}

// Хорошо: cross-cutting concerns через декоратор
public class MeteredOrderService implements OrderService {
    private final OrderService delegate;
    private final MeterRegistry metrics;

    @Override
    public Order process(OrderRequest req) {
        metrics.counter("orders.processed").increment();
        return delegate.process(req);
    }
}

Производительность

Сравнение для системы обработки заказов (100K заказов/час):

Подход к SRP Размер класса (средний) Память на объект CPU cache hit rate GC pressure
God Class (1 класс, 5000 строк) 5000 строк 2KB 60% Низкий
Строгий SRP (100 классов) 30 строк 120 байт 95% Высокий
Умеренный SRP (15 классов) 200 строк 400 байт 85% Средний

Для большинства enterprise-систем умеренный SRP — оптимальный баланс. Для highload с strict latency budget — строгий SRP может быть полезен (лучше cache locality), но overhead на аллокации нужно компенсировать object pooling.

Production Experience

War Story: Billing System Refactoring (2024)

Крупная telecom-компания, биллинговая система на Java 17. BillingEngine — 12,000 строк, 67 зависимостей, LCOM4 = 8. Над ним работали 4 команды одновременно. Каждый релиз — 3-4 регрессионных бага, связанных с тем, что одна команда ломала функциональность другой.

Диагностика:

# SonarQube показал:
# - Cognitive Complexity: 180 (лимит 15)
# - LCOM4: 8.0 (норма <= 1)
# - Number of dependencies: 67 (рекомендация <= 10)

Рефакторинг:

BillingEngine (12,000 строк)
├── TariffCalculator (800 строк) — расчёт тарифов
├── UsageAggregator (600 строк) — агрегация потребления
├── InvoiceGenerator (1,200 строк) — генерация счетов
├── DiscountEngine (900 строк) — скидки и бонусы
├── TaxCalculator (500 строк) — налоги
├── PaymentProcessor (700 строк) — обработка платежей
├── NotificationDispatcher (400 строк) — уведомления
└── BillingOrchestrator (300 строк) — координация

Результат:

  • LCOM4: 8.0 → 1.0 для каждого класса
  • Регрессионные баги: 3-4 на релиз → 0-1
  • Parallel development: 4 команды без конфликтов
  • Time to add new tariff type: 2 недели → 2 дня

War Story: Microservices Decomposition (2023)

E-commerce монолит, UserService — 6,000 строк. Команда аутентификации и команда профилей постоянно конфликтовали при merge. LCOM4 = 5, Dependencies = 42.

Разделение по границам ответственности команд:

  • AuthService (аутентификация, авторизация, токены)
  • ProfileService (данные пользователя, предпочтения)
  • NotificationPreferencesService (настройки уведомлений)

Результат: merge-конфликты сократились на 90%, деплои стали независимыми.

Monitoring и диагностика

  • ArchUnit: Автоматические проверки в CI/CD: ```java @ArchTest static final ArchRule classes_should_have_single_responsibility = classes() .that().resideInAPackage(“..domain..”) .should().haveLessThan10Dependencies() .andShould().haveSimpleNameNotContaining(“And”) .andShould().haveSimpleNameNotContaining(“Manager”) .andShould().haveSimpleNameNotContaining(“Processor”) .as(“Domain classes should follow SRP”);

@ArchTest static final ArchRule no_god_classes = noClasses() .that().resideInAPackage(“..service..”) .should().dependOnMoreThan(10Classes()) .as(“No God classes in service layer”); ```

  • SonarQube метрики:
    • Cognitive Complexity: >15 → сигнал нарушения SRP
    • LCOM4: >1 → класс можно разделить
    • Number of dependencies: >10 → потенциальный God Class
    • Class size: >500 строк → review на SRP
  • Dependency Graph: JDepend или IntelliJ IDEA Dependency Matrix. Если один класс имеет связи со всем — это God Class.

  • Code Review Rule: Если вы не можете объяснить назначение класса за 10 секунд — его нужно рефакторить.

Best Practices для Highload

  • Cohesion > Size: Маленький класс != SRP. Большой класс != нарушение SRP. Смотрите на связность (cohesion), а не на размер.
  • Data Locality: В highload группируйте по pattern доступа, а не только по домену. Данные, которые читаются вместе, должны быть вместе (cache locality).
  • Избегайте преждевременного разделения (Avoid Premature Splitting): Не разделяйте класс, пока не увидите реальные изменения от разных заинтересованных сторон (stakeholders). Принцип YAGNI (“вам это не понадобится”) применяется и к SRP.
  • Use Command/Query Separation (CQS) (Command-Query Separation — разделение методов на команды, изменяющие состояние, и запросы, только читающие данные): Разделяйте классы, которые изменяют состояние, и классы, которые только читают. Это естественное разделение ответственности.
  • Java 21 Records: Records — это естественное SRP для данных. Их единственная ответственность — хранение иммутабельных данных. equals, hashCode, toString — автоматически генерируемые, не считаются отдельной ответственностью.
  • Module System (JPMS): Java 9+ модули помогают enforcing SRP на уровне пакетов. exports и requires формализуют границы ответственности.
  • AI-assisted Refactoring: Современные IDE (IntelliJ IDEA с AI Assistant) предлагают автоматическое выделение ответственностей из больших классов на основе анализа использования полей и методов.

Резюме

  • SRP — это про границы ответственности, а не про размер класса.
  • Класс должен быть чёрным ящиком для одной конкретной задачи.
  • Высокая связность (Cohesion) внутри — признак правильного SRP.
  • Доверяйте интуиции: если класс кажется “грязным” и “раздутым” — скорее всего, так оно и есть.
  • Один класс — одна причина для изменений.

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

Обязательно знать:

  • 5 тестов SRP: Имени (без “и”), Изменения (кто стейкхолдер?), Зависимостей (импорты), Моков (<5), LCOM4
  • LCOM4: граф распадается на N компонентов → разделить на N классов; LCOM4 = 1 — одна ответственность
  • Социальный фактор: “Один класс — одна команда разработчиков”; разные команды = разные ответственности
  • Размер не определяет SRP: 30 строк могут делать 3 вещи, 300 строк — одну сложную
  • Dependency Count > 10+ = потенциальный God Class
  • Cross-cutting Concerns (логирование, метрики) — решать через AOP/декораторы, не мешать с бизнес-логикой
  • Cohesion > Size: смотрите на связность, а не на размер

Частые уточняющие вопросы:

  • Как JVM “видит” нарушение SRP? — Большой объект = cache line pollution (200 байт, используются 20), wasted memory
  • Что такое Anemic Domain Model и SRP? — Только геттеры/сеттеры — не нарушение SRP, но логика уходит в God Services
  • Class Explosion — это плохо? — Нет, IDE справляются с 10,000+ файлов; бойтесь одного файла на 10,000 строк
  • Data Locality в highload? — Группируйте по pattern доступа: данные, которые читаются вместе, должны быть вместе

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

  • “Класс маленький — значит SRP соблюдён” (30 строк могут делать 3 несвязанные вещи)
  • “Один публичный метод = SRP” (один метод может делегировать 10 подсистемам)
  • “Не надо разделять, пока не увижу проблем” (YAGNI верен, но разные stakeholders — уже сигнал)

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

  • [[1. Что такое принцип Single Responsibility и как его применять]]
  • [[13. Как принцип Single Responsibility связан с cohesion]]
  • [[14. Что произойдёт, если класс имеет несколько причин для изменения]]
  • [[18. Как рефакторить God Object (божественный объект)]]