Що станеться, якщо клас має кілька причин для зміни
Коли клас має кілька причин для зміни (порушення SRP), він стає крихким і складним. Будь-яка маленька зміна може зламати зовсім нерелевантну функціональність.
🟢 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
- Lock Contention: Singleton God Object → різні потоки борються за один монитор
- Рішення: Розділення на separate beans з independent locks
- Metaspace Impact: Huge classes → більше bytecode → більше metaspace
- Impact: +2-5MB на клас 5000+ рядків
- Transaction Boundaries: Один клас координує транзакцію через кілька доменів
- Рішення: Orchestrator pattern (клас-координатор, який викликає окремі сервіси в правильному порядку, але не містить їх бізнес-логіки),
@Transactionalна координаторі
- Рішення: Orchestrator pattern (клас-координатор, який викликає окремі сервіси в правильному порядку, але не містить їх бізнес-логіки),
Продуктивність
| Метрика | 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 спринтів):
- Feature Analysis: згрупували методи за доменами
- Витягли:
TaxCalculator,InvoiceGenerator,PaymentProcessor - Створили
BillingOrchestratorдля координації - 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. Як визначити, що клас має одну відповідальність]]