Питання 14 · Розділ 18

Що станеться, якщо клас має кілька причин для зміни

Коли клас має кілька причин для зміни (порушення SRP), він стає крихким і складним. Будь-яка маленька зміна може зламати зовсім нерелевантну функціональність.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Коли клас має кілька причин для зміни (порушення SRP), він стає крихким і складним. Будь-яка маленька зміна може зламати зовсім нерелевантну функціональність.

Проста аналогія: Уявіть швейцарський ніж з 50 інструментами. Якщо потрібно поміняти лезо — доведеться розібрати весь ніж. А якщо зламаєш викрутку — перестане працювати і штопор.

Приклад порушення:

// Погано: 4 різні причини для зміни
public class User {
    private String name;              // Дані

    public void validate() { }        // Валідація (міняють юристи)
    public void save() { }            // Збереження (міняють DBA)
    public String toJson() { }        // Формат (міняють фронтенд)
}

Що станеться:

  • Зміна валідації → може зламати збереження, бо обидва методи використовують спільні поля класу
  • Зміна формату JSON → потрібно протестувати все, оскільки код знаходиться в одному файлі
  • Різні команди конфліктують за один файл при merge

Як виправити:

// Добре: кожна відповідальність у своєму класі
public class User { private String name; }              // Тільки дані
public class UserValidator { void validate(User u) { } } // Тільки валідація
public class UserRepository { void save(User u) { } }    // Тільки БД
public class UserMapper { String toJson(User u) { } }    // Тільки формат

🟡 Middle Level

Як це працює

Коли клас має кілька причин для зміни, він стає “Жорстким” (Rigid) і “Крихким” (Fragile). Кожна відповідальність — це “вісь обертання змін”. Якщо їх кілька — клас руйнується під навантаженням.

Наслідки для проекту

Проблема Опис Вплив
Cognitive Load Клас з 5 відповідальностями = сотні комбінацій Розробники бояться міняти код
Fragility Зміна в одному місці ламає інше Каскадні баги
Merge Conflicts Різні команди міняють один файл У великих проектах — значна частина часу йде на вирішення конфліктів

Практичне застосування

Метрика Cognitive Complexity (SonarQube):

  • Метод > 15 → складно підтримувати
  • Клас > 200 рядків → важко втримати всю логіку в голові, росте ризик багів

Code Churn аналіз (метрика частоти змін файлу):

  • Якщо файл змінюється в більшості комітів → занадто багато причин для зміни

Типові помилки

Помилка Рішення
God Object (клас з десятками полів і методами на всі випадки життя) Розділити за відповідальностями
Butterfly Effect (1 зміна → 10 правок в інших файлах — ефект метелика) Виділити окремі сервіси
Daily merge conflicts (щоденні конфлікти при злитті гілок) Package-by-Feature (групування за фічами, а не по шарах)

Коли НЕ варто строго слідувати SRP

  • Прототипи/MVP (швидка перевірка)
  • Скрипти міграції (одноразовий код)
  • Коли вартість змін мінімальна

🔴 Senior Level

Internal Implementation: Механіка крихкості

Shared State Problem:

class GodObject {
    private Date lastProcessed;  // Використовується для логування І бізнес-логіки
    // Зміна формату дати в логах ламає розрахунки
}

Memory Layout Issues:

  • False Sharing: Поля різних відповідальностей потрапляють в одну cache line (64 bytes)
  • Impact: Ядра CPU інвалідують кеш один одного → slowdown 10-100x

Архітектурні Trade-offs

Строгий SRP:

  • ✅ Плюси: Мінімум side effects, паралельна розробка, easy testing
  • ❌ Мінуси: Class explosion, navigation complexity, communication overhead

Помірний SRP:

  • ✅ Плюси: Баланс між чистотою і практичністю
  • ❌ Мінуси: Вимагає зрілого судження, ризик поступової деградації архітектури

Edge Cases

  1. Lock Contention: Singleton God Object → різні потоки борються за один монитор
    • Рішення: Розділення на separate beans з independent locks
  2. Metaspace Impact: Huge classes → більше bytecode → більше metaspace
    • Impact: +2-5MB на клас 5000+ рядків
  3. Transaction Boundaries: Один клас координує транзакцію через кілька доменів
    • Рішення: Orchestrator pattern (клас-координатор, який викликає окремі сервіси в правильному порядку, але не містить їх бізнес-логіки), @Transactional на координаторі

Продуктивність

Метрика God Object SRP Compliant
Cognitive Complexity 50-200+ <15
L1 Cache Hit Rate 40-60% 85-95%
Lock Contention High (single monitor) Low (independent locks)
Test Execution Slow (many mocks) Fast (isolated)

False Sharing Impact:

Поле A (логування) і Поле B (бізнес-логіка) в одній cache line
→ Thread 1 пише A → invalidates Thread 2's cache for B
→ 10-100x slowdown при високій конкуренції

Thread Safety

  • Single monitor bottleneck: Всі потоки чекають один God Object
  • Independent services: Різні lock’и → parallel execution
  • Contention rate: God Object → 60-80% wait time, SRP → <10%

Production Experience

Рефакторинг BillingEngine (15,000 рядків):

Проблема:

  • 8 відповідальностей: розрахунок, валідація, БД, email, PDF, аудит, кеш, логування
  • Cognitive Complexity: 340 (норма < 15 — показник заплутаності коду від SonarQube)
  • Merge conflicts: щодня, 2-3 години на вирішення
  • Bug rate: 40% деплоїв з багами

Рішення (6 спринтів):

  1. Feature Analysis: згрупували методи за доменами
  2. Витягли: TaxCalculator, InvoiceGenerator, PaymentProcessor
  3. Створили BillingOrchestrator для координації
  4. Event-driven: BillingCompletedEvent для аудиту/emails

Результат:

  • Cognitive Complexity: 8-12 на клас
  • Merge conflicts: <1 на місяць
  • Bug rate: 8% деплоїв
  • Deployment time: 30 хв → 5 хв

Monitoring

ArchUnit:

@ArchTest
static void no_god_objects = classes()
    .should().haveLessThanNMethods(30)
    .andShould().haveLessThanNFields(15)
    .andShould().haveLessThanNDependencies(7);

SonarQube:

  • Cognitive Complexity > 15 → Code Smell
  • Class size > 300 lines → Refactoring candidate

Git Analysis:

# File change frequency
git log --oneline -- User.java | wc -l
# If >90% of commits touch this file → SRP violation

Best Practices for Highload

  • Max 300 lines per class
  • Max 15 fields per class
  • Max 7 dependencies
  • Package-by-Feature для модульності
  • ArchUnit CI checks
  • Code churn monitoring в Git

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Клас з кількома причинами для зміни = “Жорсткий” (Rigid) і “Крихкий” (Fragile)
  • Cognitive Load: сотні комбінацій → розробники бояться міняти код
  • Butterfly Effect: 1 зміна → 10 правок в інших файлах
  • False Sharing: поля різних відповідальностей в одній cache line → slowdown 10-100x
  • God Object → single monitor bottleneck → 60-80% wait time в багатопоточному середовищі
  • Orchestrator pattern вирішує проблему: координація без бізнес-логіки
  • Метрики: Cognitive Complexity > 15, Class size > 300 рядків, Dependencies > 7

Часті уточнюючі запитання:

  • Що таке Shared State Problem? — Поле використовується для різних цілей (логування І бізнес-логіка), зміна формату ламає розрахунки
  • Як God Object впливає на Thread Safety? — Один монитор на весь об’єкт → всі потоки чекають, contention rate 60-80%
  • Що таке Code Churn? — Метрика частоти змін файлу; >90% комітів чіпають файл → SRP violation
  • Який результат рефакторингу? — Cognitive Complexity 8-12, bug rate 8%, deployment time 5 хв замість 30

Червоні прапори (НЕ говорити):

  • “Невелике порушення SRP — це нормально” (каскадні баги і merge-конфлікти накопичуються)
  • “God Object можна виправити за один спринт” (реальні кейси: 6 спринтів для 15,000 рядків)
  • “SRP тільки для великих класів” (клас з 30 рядків може робити 3 речі)

Пов’язані теми:

  • [[1. Що таке принцип Single Responsibility і як його застосовувати]]
  • [[13. Як принцип Single Responsibility пов’язаний з cohesion]]
  • [[18. Як рефакторити God Object (божественний об’єкт)]]
  • [[21. Як визначити, що клас має одну відповідальність]]