Питання 1 · Розділ 18

Що таке принцип Single Responsibility і як його застосовувати

Простіше кажучи, кожен клас має займатися чимось одним. Це не означає "робити одну річ" — це означає, що клас має відповідати за одну конкретну частину функціональності системи.

Мовні версії: English Russian Ukrainian

🟢 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

Ознаки порушення:

  1. “And” Rule: якщо ви описуєте клас словами “він робить X І Y І Z” — SRP порушено
  2. Різні стейкхолдери: якщо різні відділи можуть вимагати змін в одному класі
  3. Величезний список імпортів: якщо клас імпортує занадто багато залежностей
  4. Складне тестування: якщо для тесту одного методу потрібно мокати 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);
    }
}

Типові помилки

  1. Помилка: Створення класів Utils, Helper, Manager Рішення: Розбивати їх на вузькоспеціалізовані класи за доменною областю

  2. Помилка: Spring @Service з 20+ автовайрінгами Рішення: Розділити на кілька сервісів за бізнес-можливостями

  3. Помилка: Надмірна декомпозиція (100 класів по 5 рядків) Рішення: Знайти баланс — групувати за фічами, не за технічними шарами

Коли НЕ варто строго слідувати SRP

  1. Прототипи та MVP — швидкість важливіша за архітектуру
  2. Скрипти на 50-100 рядків — поділ на 5 класів додасть складності
  3. Стабільні утиліти — якщо код не змінюється, 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

  1. Class Explosion Problem: При надмірному захопленні SRP система перетворюється на тисячі дрібних класів
    • Рішення: Групування за бізнес-можливостями (Package-by-Feature), а не за технічними шарами
  2. Merge Conflicts у великих командах: Один “популярний” клас редагується 10+ розробниками
    • Рішення: Розподіл відповідальності за доменними областями
  3. 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 спринти на рефакторинг:

  1. Виділили PaymentProcessor, InventoryChecker, NotificationService
  2. Створили OrderOrchestrator для координації
  3. Вкрили кожен компонент незалежними тестами

Результат: Кількість багів при деплої знизилася на 60%, час код-ревью скоротився вдвічі.

Monitoring та діагностика

Як виявити порушення SRP у коді:

  1. Метрики коду:
    • Кількість рядків > 300
    • Кількість методів > 10
    • Cyclomatic complexity > 15
    • Кількість залежностей > 7
  2. Інструменти:
    • SonarQube (метрики зв’язності)
    • Checkstyle (розмір класу)
    • ArchUnit (архітектурні тести)
  3. 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. Як визначити, що клас має одну відповідальність]]