Що таке принцип Single Responsibility і як його застосовувати
Простіше кажучи, кожен клас має займатися чимось одним. Це не означає "робити одну річ" — це означає, що клас має відповідати за одну конкретну частину функціональності системи.
🟢 Junior Level
Принцип єдиної відповідальності (SRP — Single Responsibility Principle) — це один із п’яти принципів SOLID, який стверджує: “У класу має бути лише одна причина для зміни.”
Причина для зміни — вимога від конкретного стейкхолдера або системи. Наприклад: бухгалтерія просить змінити формулу зарплати (одна причина), а IT — змінити драйвер БД (інша причина). Якщо два різних відділи можуть попросити змінити один клас — SRP порушено.
Простіше кажучи, кожен клас має займатися чимось одним. Це не означає “робити одну річ” — це означає, що клас має відповідати за одну конкретну частину функціональності системи.
Проста аналогія: Уявіть ресторан. Кухар готує їжу, офіціант обслуговує клієнтів, касир приймає оплату. Якщо одна людина робитиме все одразу — виникне хаос.
Приклад порушення SRP:
// Погано: клас робить занадто багато
public class Employee {
public void calculateSalary() { /* розрахунок зарплати */ }
public void saveToDatabase() { /* збереження в БД */ }
public void generateReport() { /* генерація звіту */ }
public void sendEmail() { /* відправка email */ }
}
Приклад з дотриманням SRP:
// Добре: кожна відповідальність у своєму класі
public class Employee {
// Тільки дані співробітника
private String name;
private BigDecimal salary;
}
public class SalaryCalculator {
public BigDecimal calculate(Employee emp) { /* розрахунок */ }
}
public class EmployeeRepository {
public void save(Employee emp) { /* збереження в БД */ }
}
public class EmailService {
public void sendNotification(Employee emp) { /* відправка email */ }
}
Коли використовувати:
- Як орієнтир — дотримуйтесь SRP у більшості випадків. Винятки: прототипи, MVP, стабільні утиліти, скрипти на 50-100 рядків.
- При створенні нових класів
- При рефакторингу існуючого коду
🟡 Middle Level
Як це працює
SRP тісно пов’язаний з поняттями:
- Cohesion (зв’язність) — наскільки методи класу працюють для однієї мети. Чим вища — тим краще.
- Coupling (зчеплення) — наскільки клас залежить від інших класів. Чим менше — тим краще.
Як виявити порушення SRP
Ознаки порушення:
- “And” Rule: якщо ви описуєте клас словами “він робить X І Y І Z” — SRP порушено
- Різні стейкхолдери: якщо різні відділи можуть вимагати змін в одному класі
- Величезний список імпортів: якщо клас імпортує занадто багато залежностей
- Складне тестування: якщо для тесту одного методу потрібно мокати 5+ залежностей
Практичне застосування
// Порушення SRP: OrderProcessor робить все
public class OrderProcessor {
public void process(Order order) {
validateOrder(order); // Валідація
saveOrder(order); // Робота з БД
sendConfirmation(order); // Відправка повідомлень
logToAnalytics(order); // Логування
updateInventory(order); // Оновлення інвентарю
}
}
// Дотримання SRP: делегування відповідальностей
public class OrderProcessor {
private final OrderValidator validator;
private final OrderRepository repository;
private final NotificationService notifier;
private final AnalyticsLogger logger;
private final InventoryService inventory;
public void process(Order order) {
validator.validate(order);
repository.save(order);
notifier.sendConfirmation(order);
logger.log(order);
inventory.update(order);
}
}
Типові помилки
-
Помилка: Створення класів
Utils,Helper,ManagerРішення: Розбивати їх на вузькоспеціалізовані класи за доменною областю -
Помилка: Spring
@Serviceз 20+ автовайрінгами Рішення: Розділити на кілька сервісів за бізнес-можливостями -
Помилка: Надмірна декомпозиція (100 класів по 5 рядків) Рішення: Знайти баланс — групувати за фічами, не за технічними шарами
Коли НЕ варто строго слідувати SRP
- Прототипи та MVP — швидкість важливіша за архітектуру
- Скрипти на 50-100 рядків — поділ на 5 класів додасть складності
- Стабільні утиліти — якщо код не змінюється, SRP не потрібен
🔴 Senior Level
Internal Implementation та Архітектура
SRP — це принцип управління змінами, а не просто “один клас — одна функція”. Оригінальне визначення Роберта Мартіна (Uncle Bob) звучить так:
“У модуля має бути лише одна причина для зміни”
На рівні Senior важливо розуміти: SRP застосовується не лише до класів, але й до модулів, сервісів і навіть мікросервісів.
Рівні Cohesion (зв’язності)
Senior-розробник має розрізняти типи зв’язності:
| Тип Cohesion | Опис | Приклад |
|---|---|---|
| Functional (найкраща) | Усі частини класу необхідні для однієї функції | BCryptPasswordEncoder |
| Sequential | Вихід одного методу — вхід для іншого | OrderPipeline |
| Communicational | Методи працюють з одними даними | UserService з полями користувача |
| Procedural | Методи виконуються у певному порядку | TransactionManager |
| Temporal | Методи виконуються в один час | StartupInitializer |
| Utility | Слабка зв’язність, випадкове групування | CommonUtils ❌ |
| Coincidental (найгірша) | Частини опинилися разом випадково | Helper клас ❌ |
Архітектурні Trade-offs
Строге дотримання SRP:
- ✅ Плюси: Легке тестування, низьке зчеплення, висока зв’язність, мінімум merge conflicts
- ❌ Мінуси: Велика кількість класів, складність навігації, ризик “атомарного пилу” (atomic dust)
Помірне дотримання SRP:
- ✅ Плюси: Баланс між читабельністю та гнучкістю
- ❌ Мінуси: Вимагає зрілого судження, ризик поступового погіршення
Edge Cases
- Class Explosion Problem: При надмірному захопленні SRP система перетворюється на тисячі дрібних класів
- Рішення: Групування за бізнес-можливостями (Package-by-Feature), а не за технічними шарами
- Merge Conflicts у великих командах: Один “популярний” клас редагується 10+ розробниками
- Рішення: Розподіл відповідальності за доменними областями
- Cognitive Load: Величезні класи (God Objects) неможливо утримати в голові
- Рішення: Обмеження розміру класу (200-300 рядків як орієнтир)
Продуктивність
- Memory footprint: Більше класів → більше метаданих у Metaspace. На практиці це непомітно для JVM (кілька KB на клас)
- Method dispatch: Делегування додає виклики методів. JIT-компілятор зазвичай робить inlining, зводячи оверхед до нуля
- Startup time: Велика кількість дрібних класів може незначно сповільнити старт (завантаження метаданих)
Production Experience
Реальний сценарій із продакшену:
У великому e-commerce проекті був OrderService з 3000+ рядків коду та 25 залежностями. Будь-яка зміна призводила до регресійних багів. Команда витратила 3 спринти на рефакторинг:
- Виділили
PaymentProcessor,InventoryChecker,NotificationService - Створили
OrderOrchestratorдля координації - Вкрили кожен компонент незалежними тестами
Результат: Кількість багів при деплої знизилася на 60%, час код-ревью скоротився вдвічі.
Monitoring та діагностика
Як виявити порушення SRP у коді:
- Метрики коду:
- Кількість рядків > 300
- Кількість методів > 10
- Cyclomatic complexity > 15
- Кількість залежностей > 7
- Інструменти:
- SonarQube (метрики зв’язності)
- Checkstyle (розмір класу)
- ArchUnit (архітектурні тести)
- Code Review ознаки:
- Різні ревьюери для різних частин класу
- Часті конфлікти в Git
- Складність написання unit-тестів
Best Practices для Highload
- SRP у мікросервісах: Один мікросервіс — одна бізнес-задача (аналогія SRP на системному рівні)
- Feature Toggles: Якщо одна фіча вимагає зміни в 5+ сервісах — можливо, межі сервісів проведені неправильно
- Domain-Driven Design: Aggregate Root має мати одну відповідальність у межах свого Aggregate
Зв’язок з іншими принципами
- SRP → OCP: Коли у класу одна відповідальність, його легше розширювати без модифікації
- SRP → LSP: Маленькі класи з однією метою легше правильно наслідувати
- SRP → ISP: Розподіл інтерфейсів природно випливає з поділу відповідальності
- SRP → DIP: Делегування залежностей призводить до інверсії залежностей
Резюме для Senior
- SRP — це про управління змінами, а не про кількість рядків
- Прагніть до Functional Cohesion, уникайте Coincidental Cohesion
- SRP зменшує крихкість системи: зміна логіки відправки пошти не повинна ламати розрахунок цін
- Використовуйте ArchUnit для автоматичної перевірки архітектурних меж
- Пам’ятайте про баланс: не перетворюйте код на “атомарний пил” (atomic dust)
- SRP — це інвестиція у підтримуваність коду на довгі роки
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- SRP: у класу має бути лише одна причина для зміни
- Причина для зміни = вимога від конкретного стейкхолдера
- Висока cohesion (зв’язність) — природний результат дотримання SRP
- Порушення SRP призводить до God Object, крихкості та merge-конфліктів
- SRP застосовується не лише до класів, але й до модулів, сервісів, мікросервісів
- Уникайте класів
Utils,Helper,Manager— розбивайте за доменними областями - Spring
@Serviceз 20+ автовайрінгами — явна ознака порушення SRP - SRP зменшує крихкість системи: зміна однієї частини не ламає іншу
Часті уточнюючі питання:
- Як виявити порушення SRP? — LCOM > 1.0, >300 рядків, >10 методів, >7 залежностей, складні тести з 5+ моками
- Коли можна порушити SRP? — Прототипи/MVP, скрипти до 100 рядків, стабільні утиліти
- Що таке Atomic Dust? — Надмірна декомпозиція, коли система перетворюється на тисячі дрібних класів
- Як SRP пов’язаний з іншими принципами SOLID? — SRP робить код тестованим (DIP), розширюваним (OCP), успадковуваним (LSP)
Червоні прапорці (НЕ говорити):
- “SRP означає, що клас має мати лише один метод” (ні — одну відповідальність, а не одну дію)
- “Я завжди слідую SRP без винятків” (ні — прототипи та скрипти не потребують)
- “Чим більше класів — тим краще” (ні — баланс між зв’язністю та кількістю файлів)
Пов’язані теми:
- [[2. Наведіть приклад порушення принципу Single Responsibility]]
- [[13. Як принцип Single Responsibility пов’язаний з cohesion]]
- [[14. Що станеться, якщо клас має кілька причин для зміни]]
- [[18. Як рефакторити God Object (божественний об’єкт)]]
- [[21. Як визначити, що клас має одну відповідальність]]