Як визначити, що клас має одну відповідальність
Визначення того, чи має клас одну відповідальність (Single Responsibility Principle, SRP) — це одна з ключових практичних навичок розробника.
🟢 Junior Level
Визначення того, чи має клас одну відповідальність (Single Responsibility Principle, SRP) — це одна з ключових практичних навичок розробника.
Роберт Мартін дав точне формулювання:
«Клас має одну відповідальність, якщо у нього є одна і тільки одна причина для зміни — тобто він відповідає перед єдиним бізнес-актором.»
Простий тест «Елеваторного пітчу» (The Elevator Pitch Test)
Спробуйте описати призначення класу одним простим реченням:
- Якщо в описі природним чином виникають сполучники «І», «АБО», «А ТАКОЖ» — у класу кілька відповідальностей!
- Погано: «Клас
OrderManagerперевіряє валідність замовлення, ТА розраховує знижку, ТА зберігає рядок у БД, ТА надсилає чек клієнту на email». - Добре: «Клас
OrderValidatorперевіряє коректність полів замовлення». - Добре: «Клас
OrderRepositoryзберігає та витягує сутності замовлень із бази даних».
Наочний приклад: Визначення розмитої відповідальності
// Занадто багато обов'язків в одному класі:
public class UserService {
// 1. Обов'язок перед фахівцями з безпеки: хешування пароля
public String hashPassword(String rawPassword) { /* ... */ }
// 2. Обов'язок перед DBA: SQL-запити до таблиці users
public void saveUserToPostgres(User user) { /* ... */ }
// 3. Обов'язок перед відділом комунікацій: відправка SMS
public void sendWelcomeSms(String phone) { /* ... */ }
// 4. Обов'язок перед фронтендом: форматування відповіді в JSON
public String mapToJson(User user) { /* ... */ }
}
Як виправити:
Рознести функціонал по 4 спеціалізованих класах: PasswordEncoder, UserRepository, SmsNotificationService, UserJsonMapper.
🟡 Middle Level
5 інженерних тестів для точної діагностики SRP
У промисловій розробці Senior-інженери використовують 5 чітких діагностичних тестів для виявлення прихованих порушень SRP:
1. Тест бізнес-актора (The Actor Test)
Поставте запитання: «Хто з реальних людей у компанії може вимагати змінити цей файл?»
- Якщо вимоги на доопрацювання можуть прийти одночасно від бухгалтера (формула податку), юриста (текст оферти) та DevOps-інженера (таймаути з’єднань) — клас порушує SRP. У класу повинна бути рівно одна зацікавлена особа (одна бізнес-роль).
2. Тест списку імпортів (The Import Analysis Test)
Відкрийте верхню частину Java-файлу і погляньте на список пакетів import:
import java.sql.Connection; // Доступ до даних
import jakarta.mail.Transport; // Поштовий протокол
import com.fasterxml.jackson.databind.*; // JSON-серіалізація
import org.springframework.web.bind.*; // HTTP/REST контролери
Якщо клас імпортує одночасно SQL-драйвери, поштові клієнти, мережеві парсери та анотації контролерів — він грубо порушує межі архітектурних шарів та SRP.
3. Тест обсягу залежностей конструктора (The Dependency Count Test)
Якщо конструктор класу приймає понад 5–7 залежностей:
public OrderService(UserRepo u, OrderRepo o, EmailService e, SmsService s,
PdfService p, TaxCalc t, DiscountCalc d, AuditLog a) // 8 залежностей!
Це вірний симптом того, що клас перетворився на «божественний об’єкт» (God Object) і стягує він на себе чужу роботу. Норма для сфокусованого класу — 2–4 залежності.
4. Тест моків у юніт-тестах (The Mocking Burden Test)
Спробуйте протестувати один метод класу. Якщо для перевірки банального if-else вам доводиться писати 30 рядків налаштування моків (when(mock1...).thenReturn(...); when(mock2...).thenReturn(...)), клас перевантажений сторонніми обов’язками.
5. Граф зв’язності методів LCOM4 (Lack of Cohesion of Methods)
Статичний аналізатор (SonarQube) будує граф: вузли — методи та поля, ребра — звернення до полів.
- Якщо граф розпадається на 2 або більше ізольованих компонент зв’язності ($\text{LCOM4} \ge 2$), клас усередині фактично складається з кількох незалежних програм. Його необхідно розділити на кількість частин, що дорівнює значенню LCOM4.
🔴 Senior Level
Соціально-організаційний аспект: Закон Конвея
Роберт Мартін підкреслює: SRP — це не стільки про кількість рядків коду, скільки про організаційну структуру компанії (Conway’s Law):
[Команда Білінгу] [Команда Профілю Користувача]
│ │
└───────────────►[User.java]◄───────────┘
(Точка зіткнення в Git!)
Якщо один Java-клас регулярно редагують розробники з двох різних продуктових команд (наприклад, команда каталогу та команда рекомендацій), клас порушує SRP. Рано чи пізно одна команда ненавмисно зламає логіку іншої.
Архітектурне рішення: Розподіл за обмеженими контекстами (Bounded Contexts у Domain-Driven Design) — у кожної команди з’являється своя модель користувача (BillingUser та ProfileUser), незалежні одна від одної.
CQS (Command-Query Separation) як природна межа SRP
Патерн Бертрана Меєра Command-Query Separation (CQS) та його архітектурний розвиток CQRS задають ідеальну межу розділення відповідальності:
- Команди (Commands): Методи, що мутують стан системи (повертають
void, містять транзакції, викликають валідацію та запис у БД:CreateOrderCommand,CancelSubscriptionCommand). - Запити (Queries): Методи, що лише читають дані (ідемпотентні, не мають побічних ефектів, оптимізовані під швидке читання:
GetOrderSummaryQuery).
Клас, що поєднує складну бізнес-модифікацію та монструозні SQL-вибірки для відображення звітів, порушує SRP. Розділення на Command-сервіси та Query-сервіси робить кодову базу передбачуваною та легко масштабованою.
Ізоляція наскрізної функціональності (Cross-Cutting Concerns)
Часта помилка — включення технічної наскрізної функціональності (логування, збір метрик Micrometer, розподілений трейсинг OpenTelemetry, перевірка прав Spring Security) у доменні бізнес-сервіси:
// Погано: бізнес-метод засмічений сторонніми обов'язками
public Order processOrder(OrderRequest req) {
long start = System.currentTimeMillis();
log.info("Starting order processing for user: {}", req.userId());
securityContext.checkPermission("ORDER_CREATE");
// Реальна бізнес-логіка (3 рядки)
Order order = new Order(req);
orderRepository.save(order);
metrics.timer("order.process.time").record(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
log.info("Order processed successfully: {}", order.getId());
return order;
}
Рішення за Senior-стандартом: Наскрізні обов’язки виносяться в Spring AOP-аспекти, HTTP Interceptors / Filters або реалізуються через патерн Decorator. Бізнес-сервіс повинен містити виключно правила предметної області.
4 Tricky Questions
1. Чи може клас із 20 рядків порушувати SRP, а клас із 600 рядків — ідеально йому відповідати?
Відповідь: Так, безумовно. Розмір класу в рядках (LOC) не визначає кількість його відповідальностей:
- Клас із 20 рядків може містити 3 методи: розпарсити CSV, відправити REST-запит і записати помилку у файл — це 3 різні відповідальності і 3 різних актори (порушення SRP).
- Клас із 600 рядків може являти собою парсер математичних виразів (Abstract Syntax Tree Parser) або калькулятор складного податкового законодавства. Він вирішує рівно одне математичне завдання, має одного бізнес-актора (податковий комітет) та згуртованість $\text{LCOM4} = 1$. Дробити такий клас безглуздо.
2. Чи є клас-оркестратор (Orchestrator / Coordinator) порушенням SRP, якщо він викликає 10 різних сервісів?
Відповідь:
Ні, якщо він спроєктований правильно.
В оркестратора є своя власна єдина відповідальність — координація бізнес-процесу (Workflow Coordination).
Він знає послідовність кроків алгоритму, але не знає, як саме працює кожен крок. Якщо оркестратор сам не обчислює податки, не будує SQL-запити і не форматує JSON, а лише послідовно делегує кроки (validator.validate() -> payment.charge() -> notifier.send()), він суворо дотримується SRP.
3. Як розрізнити «схожий код» (Accidental Duplication) та «істинне порушення SRP» при рефакторингу?
Відповідь: Це фундаментальна дилема між принципом DRY та SRP. Якщо два методи в модулі реєстрації користувачів та модулі оформлення замовлення перевіряють номер телефону абсолютно однаковим регулярним виразом:
- Це випадковий збіг (Accidental Duplication), якщо правила валідації телефону для безпеки акаунту та для служби доставки можуть у майбутньому змінитися незалежно (різні актори). Винесення їх у спільний утилітарний клас зв’яже два незалежні домени жорстким зв’язком.
- Істинне порушення SRP виникає, коли код змінюється з однієї й тієї самої бізнес-причини від одного актора. Тільки в цьому випадку логіку слід об’єднувати.
4. Чому патерн «Анемічна доменна модель» (Anemic Domain Model) оманливо виглядає як дотримання SRP?
Відповідь:
Анемічна модель розділяє дані (клас-сутність лише з полями та гетерами/сетерами) і логіку (сервісний клас із методами обробки).
Формально здається, що сутність ідеальна за SRP: вона «тільки зберігає дані». Але на практиці це руйнує інкапсуляцію ООП: сутність не може захистити свої бізнес-інваріанти (будь-який сервіс може викликати order.setStatus(null)), а сервіси навколо неї розростаються в багатотисячні God Objects, концентруючи в собі логіку всіх мислимих сценаріїв. Rich Domain Model (Багата модель у DDD) поєднує дані з операціями, що захищають інваріанти, зберігаючи SRP на рівні агрегату.
🎯 Шпаргалка для інтерв’ю
Головні критерії (30 секунд на відповідь):
- Визначення Мартіна: Один клас — один бізнес-актор (одна причина для зміни).
- Швидкі тести на SRP:
- Пітч-тест: Опис призначення класу без сполучників «І» / «А ТАКОЖ».
- Тест актора: Скільки різних відділів компанії можуть замовити доопрацювання файлу?
- Тест імпортів: Чи немає в одному файлі
java.sql,jakarta.mailтаcom.fasterxml.jackson? - Тест моків: Чи потрібно налаштовувати $> 4$ моків заради одного модульного тесту?
- LCOM4: Чи дорівнює 1 граф зв’язності методів і полів?
- Наскрізна функціональність: Логування, аудит та метрики виносяться в AOP/декоратори, не засмічуючи домен.
Чого уникати (Red Flags):
- ❌ Оцінювати SRP виключно за кількістю рядків коду або кількістю методів.
- ❌ Вважати, що класи з суфіксами
Manager,Processor,Helperмають одну відповідальність (у 95% випадків це God Objects). - ❌ Розділяти семантично нерозривний клас на 10 класів по 3 рядки заради «абсолютного SRP» (ризик антипатерна Atomic Dust).