Питання 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. Помилка: Мікс бізнес-логіки та інфраструктури Рішення: Виділяти роботу з БД, HTTP, чергами в окремі шари

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

Крок 1: Виявити різні відповідальності Крок 2: Створити окремі класи для кожної Крок 3: Використати композицію для делегування Крок 4: Оригінальний клас стає координатором (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]]