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

Приведите пример нарушения принципа Single Responsibility

Нарушение SRP создаёт неявные зависимости в системе. Изменение одной маленькой детали (например, формата даты в логах) может сломать критический бизнес-процесс (например, сохран...

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

🟢 Junior Level

Нарушение принципа Single Responsibility (SRP) происходит, когда класс занимается несколькими несвязанными вещами одновременно. Такой класс часто называют God Object (Божественный объект) — он знает и умеет слишком много.

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

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

// Плохо: OrderProcessor делает слишком много
public class OrderProcessor {
    
    public void processOrder(Order order) {
        // 1. Валидация заказа
        if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Invalid amount");
        }
        
        // 2. Сохранение в базу данных
        saveToDatabase(order);
        
        // 3. Отправка email клиенту
        sendEmail(order);
        
        // 4. Логирование
        logToSystem(order);
    }
    
    private void saveToDatabase(Order order) { /* 50 строк кода */ }
    private void sendEmail(Order order) { /* 50 строк кода */ }
    private void logToSystem(Order order) { /* 50 строк кода */ }
}

Почему это плохо:

  • Если изменится способ отправки email — придётся менять класс, который отвечает за заказы
  • Сложно тестировать: для проверки валидации нужно мокать базу данных и почту
  • Невозможно переиспользовать: нельзя использовать saveToDatabase отдельно

Как исправить:

// Хорошо: каждая ответственность в своём классе
public class OrderProcessor {
    private final OrderValidator validator;
    private final OrderRepository repository;
    private final EmailService emailService;
    private final Logger logger;

    public void processOrder(Order order) {
        validator.validate(order);
        repository.save(order);
        emailService.sendOrderConfirmation(order);
        logger.log("Order processed: " + order.getId());
    }
}

🟡 Middle Level

Анатомия нарушения SRP

Нарушение SRP создаёт неявные зависимости в системе. Изменение одной маленькой детали (например, формата даты в логах) может сломать критический бизнес-процесс (например, сохранение заказа), потому что всё это живёт в одном файле.

Типичные примеры нарушения SRP

1. Spring @Service — “помойка”

Частый антипаттерн в Spring-проектах: один сервис с десятком репозиториев.

@Service
public class GeneralService {
    @Autowired private UserRepository userRepo;
    @Autowired private OrderRepository orderRepo;
    @Autowired private ProductRepository productRepo;
    @Autowired private PromoRepository promoRepo;
    @Autowired private AuditRepository auditRepo;
    // ... ещё 15 репозиториев

    // Каждый @Autowired создаёт прокси-объект. 20+ зависимостей = 20+ прокси
    // = сложная цепочка вызовов. При тестировании нужно мокать каждый из 20.

    // Этот метод нарушает SRP: объединяет логику 5 разных доменов
    public void checkout(Long userId, Long cartId) {
        // логика на 200 строк
    }
}

Следствие: Любой merge в Git вызывает конфликт, так как 10 разработчиков меняют этот файл ежедневно.

2. God Object в бизнес-логике

public class EmployeeManager {
    // HR ответственность
    public void hireEmployee(Employee emp) { }
    public void fireEmployee(Long id) { }
    
    // Бухгалтерия
    public BigDecimal calculateSalary(Employee emp) { }
    public BigDecimal calculateTax(Employee emp) { }
    
    // IT инфраструктура
    public void createEmailAccount(Employee emp) { }
    public void assignLaptop(Employee emp) { }
    
    // Отчётность
    public void generateDepartmentReport() { }
}

Проблема: Разные стейкхолдеры (HR, бухгалтерия, IT) требуют изменений в одном классе.

Диагностика нарушения SRP

Признак Что означает
“And” Rule Если описание класса содержит “и” (“он делает X И Y И Z”)
Mocks Overload В unit-тесте больше 5 моков
Import List Size Список импортов занимает >20 строк
Cyclomatic Complexity Много веток if/else для разных сценариев. Cyclomatic Complexity — количество независимых путей выполнения в методе. Считается как число веток if/else/switch + 1. CC = 10 означает 10+ тестов для полного покрытия. Чем выше — тем сложнее тестировать.
Merge Conflicts Частые конфликты в Git при редактировании файла

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

  1. Ошибка: Создание классов Utils, Helper, Manager Решение: Разбивать по доменным областям (PasswordHasher, DateFormatter). В DDD Manager/Orchestrator — легитимные паттерны координации. Проблема не в названии, а в том, что класс смешивает бизнес-логику с инфраструктурой.

  2. Ошибка: Объединение CRUD операций в один сервис Решение: Разделять по бизнес-возможностям (OrderCreator, OrderCanceler)

  3. Ошибка: Mix бизнес-логики и инфраструктуры Решение: Выделять работу с БД, HTTP, очередями в отдельные слои

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

Шаг 1: Выявить разные ответственности Шаг 2: Создать отдельные классы для каждой Шаг 3: Использовать композицию для делегирования Шаг 4: Original класс становится координатором (Orchestrator)


🔴 Senior Level

Internal Implementation: Почему God Object — это “яд” для проекта

Нарушение SRP создаёт фундаментальные проблемы на архитектурном уровне:

1. Хрупкость (Fragility)

Когда мы заменяем одну зависимость (например, Splunk → ELK), нам приходится перетестировать всю логику класса, потому что все методы живут в одном файле и могут разделять общие поля или состояние.

// Хрупкий God Object
public class OrderProcessor {
    private final HttpClient httpClient;  // Используется для email И логирования
    private final Connection dbConnection; // Используется для заказов И аудита
    
    public void process(Order order) {
        // Если httpClient сломается — упадёт и email, и логирование
        // Невозможно обновить логирование без риска сломать email
    }
}

2. Неподвижность (Immobility)

God Object невозможно переиспользовать, не потянув за собой все его зависимости. Это нарушает принцип Mobility — способность компонентов работать в разных контекстах.

3. Низкая тестируемость

// Тест для God Object — кошмар
@Test
void testProcessOrder() {
    // Нужно мокать: БД, email, логирование, аналитику, инвентарь...
    when(mockDb.save(any())).thenReturn(true);
    when(mockEmail.send(any())).thenReturn(true);
    when(mockLog.log(any())).thenReturn(true);
    when(mockAnalytics.track(any())).thenReturn(true);
    when(mockInventory.reserve(any())).thenReturn(true);
    // 20 строк моков для 5 строк реальной логики
}

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

Строгая декомпозиция:

  • ✅ Плюсы: Идеальная тестируемость, независимые деплои, чёткие границы
  • ❌ Минусы: Много классов, сложность навигации, оверхед на коммуникацию

Умеренная декомпозиция:

  • ✅ Плюсы: Баланс читаемости и гибкости
  • ❌ Минусы: Требует зрелого суждения, постепенное ухудшение

Edge Cases

  1. Transaction Boundaries: Когда одна транзакция затрагивает несколько доменов
    • Решение: Использовать @Transactional на уровне orchestrator’а, но бизнес-логика в отдельных сервисах
  2. CQRS Pattern: Разделение Command и Query ответственности
    • Пример: OrderCommandService (создание/удаление) vs OrderQueryService (чтение)
  3. Event-Driven Architecture: Разделение через события
    • Пример: OrderCreatedEvent → отдельные обработчики для Email, Analytics, Inventory

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

  • Method call overhead: Делегирование добавляет вызовы методов. JIT делает inlining, оверхед ~0
  • Memory: Больше объектов → больше ссылок. GC справляется эффективно (поколенческий GC)
  • ClassLoader: Больше классов → больше метаданных в Metaspace (несколько KB на класс)

Production Experience

Реальная история из enterprise проекта:

В банковском проекте был PaymentProcessor на 5000+ строк с 40 зависимостями. Проблемы:

  • Любое изменение занимало 3-4 дня (анализ + тесты)
  • 30% всех багов в проде были из-за побочных эффектов
  • Merge конфликты каждый день
  • Новые разработчики входили в проект 2-3 месяца

Решение: Постепенный рефакторинг за 4 спринта:

  1. Выделили PaymentValidator, PaymentRepository, NotificationService
  2. Создали FraudDetectionService, ComplianceChecker
  3. PaymentOrchestrator координирует вызовы
  4. Event-driven коммуникация через Spring Events

Результат:

  • Количество багов снизилось на 70%
  • Время код-ревью сократилось с 2 часов до 30 минут
  • Новые фичи стали добавляться за 1-2 дня вместо недели

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

Статический анализ:

// ArchUnit тест для проверки SRP
@ArchTest
static void services_should_have_single_responsibility = classes()
    .that().resideInAPackage("..service..")
    .should().haveSimpleNameNotContaining("Manager")
    .andShould().haveSimpleNameNotContaining("Utils")
    .andShould().haveLessThanNDependencies(7);

Метрики для отслеживания:

  • Количество строк на класс (< 300)
  • Количество методов (< 10)
  • Cyclomatic complexity (< 15)
  • Количество зависимостей (< 7)
  • Количество моков в тестах (< 5)

Best Practices для Highload

  • Domain-Driven Design: Aggregate Root имеет одну ответственность в пределах Aggregate
  • Package-by-Feature: Группировка по бизнес-возможностям, не по техническим слоям
  • Hexagonal Architecture: Разделение domain, application и infrastructure слоёв
  • Event Sourcing: Разделение состояний через событийную модель

Связь с другими паттернами

  • SRP + Decorator: Добавление ответственности без нарушения SRP
  • SRP + Chain of Responsibility: Каждый обработчик — одна ответственность
  • SRP + Strategy: Каждая стратегия — одна ответственность

Резюме для Senior

  • Нарушение SRP превращает систему в “карточный домик” — изменение одной детали ломает всё
  • Ищите признаки God Object: огромные методы, десятки зависимостей, частые merge конфликты
  • Используйте декомпозицию по бизнес-смыслу, а не по техническим функциям
  • Помните: SRP делает код чистым, а не мелким — балансируйте между связностью и количеством файлов
  • ArchUnit — ваш друг для автоматической проверки архитектурных границ
  • SRP — это инвестиция в поддерживаемость, а не “бюрократия”

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

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

  • God Object — класс, который знает и умеет слишком много, нарушает SRP
  • “And” Rule: если описание класса содержит “и” — SRP нарушен
  • Spring @Service с 20+ репозиториями — типичный God Object в enterprise
  • Рефакторинг: выявить ответственности → создать отдельные классы → делегирование
  • Нарушение SRP создаёт хрупкость, неподвижность и низкую тестируемость
  • Characterization Tests — первый шаг рефакторинга legacy без тестов
  • Event-driven архитекция помогает разделить ответственность через события

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

  • Как диагностировать God Object? — >300 строк, >10 методов, >7 зависимостей, >5 моков в тестах
  • Что такое Orchestrator? — Координатор, который делегирует работу отдельным сервисам, не содержит бизнес-логики
  • Как рефакторить без тестов? — Characterization Tests / Golden Master: зафиксировать текущее поведение, потом разделять
  • CQRS и SRP? — Разделение Command и Query — пример SRP на уровне сервисов

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

  • “Manager — нормальное название класса” (чаще всего это God Object)
  • “Если код работает — SRP не нужен” (работает сегодня, но неподдерживаемо завтра)
  • “20 зависимостей в сервисе — это нормально” (это явный признак нарушения SRP)

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

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