📐 Розділ 18 · Питання #21

Як визначити, що клас має одну відповідальність

Визначення того, чи має клас одну відповідальність (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:
    1. Пітч-тест: Опис призначення класу без сполучників «І» / «А ТАКОЖ».
    2. Тест актора: Скільки різних відділів компанії можуть замовити доопрацювання файлу?
    3. Тест імпортів: Чи немає в одному файлі java.sql, jakarta.mail та com.fasterxml.jackson?
    4. Тест моків: Чи потрібно налаштовувати $> 4$ моків заради одного модульного тесту?
    5. LCOM4: Чи дорівнює 1 граф зв’язності методів і полів?
  • Наскрізна функціональність: Логування, аудит та метрики виносяться в AOP/декоратори, не засмічуючи домен.

Чого уникати (Red Flags):

  • ❌ Оцінювати SRP виключно за кількістю рядків коду або кількістю методів.
  • ❌ Вважати, що класи з суфіксами Manager, Processor, Helper мають одну відповідальність (у 95% випадків це God Objects).
  • ❌ Розділяти семантично нерозривний клас на 10 класів по 3 рядки заради «абсолютного SRP» (ризик антипатерна Atomic Dust).

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