Наведіть приклад порушення принципу Single Responsibility
Порушення SRP створює неявні залежності в системі. Зміна однієї маленької деталі (наприклад, формату дати в логах) може зламати критичний бізнес-процес (наприклад, збереження за...
🟢 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 при редагуванні файлу |
Типові помилки
-
Помилка: Створення класів
Utils,Helper,ManagerРішення: Розбивати за доменними областями (PasswordHasher,DateFormatter). У DDDManager/Orchestrator— легітимні патерни координації. Проблема не в назві, а в тому, що клас змішує бізнес-логіку з інфраструктурою. -
Помилка: Об’єднання CRUD операцій в один сервіс Рішення: Розділяти за бізнес-можливостями (
OrderCreator,OrderCanceler) -
Помилка: Мікс бізнес-логіки та інфраструктури Рішення: Виділяти роботу з БД, 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
- Transaction Boundaries: Коли одна транзакція зачіпає кілька доменів
- Рішення: Використовувати
@Transactionalна рівні orchestrator’а, але бізнес-логіка в окремих сервісах
- Рішення: Використовувати
- CQRS Pattern: Розподіл Command та Query відповідальності
- Приклад:
OrderCommandService(створення/видалення) vsOrderQueryService(читання)
- Приклад:
- 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 спринти:
- Виділили
PaymentValidator,PaymentRepository,NotificationService - Створили
FraudDetectionService,ComplianceChecker PaymentOrchestratorкоординує виклики- 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]]