Наведіть приклад порушення принципу Single Responsibility
Уявіть шеф-кухаря елітного ресторану, який одночасно: 4. Миє підлогу в залі для відвідувачів.
🟢 Junior Level
Порушення принципу Single Responsibility (SRP) виникає тоді, коли клас бере на себе відповідальність за кілька непов’язаних завдань, змушуючи його змінюватися через різні, незалежні одна від одної причини.
Клас-порушник (God Object / Swiss Army Knife):
┌────────────────────────────────────────────────────────┐
│ OrderController │
│ ├── parseAndValidateJson() ──► Робота з HTTP/JSON │
│ ├── calculateDiscounts() ──► Бізнес-логіка знижок │
│ ├── executeSqlInsert() ──► Доступ до даних / SQL │
│ └── sendSmsViaTwilio() ──► Інтеграція з SMS-шлюзом│
└────────────────────────────────────────────────────────┘
Наочний приклад із життя:
Уявіть шеф-кухаря елітного ресторану, який одночасно:
- Готує страви на кухні.
- Сам розвозить замовлення на мопеді містом.
- Веде податковий облік і здає бухгалтерську звітність.
- Миє підлогу в залі для відвідувачів.
Якщо шеф-кухар застрягне в заторі під час доставки, робота кухні зупиниться, а бухгалтерія прострочить сплату податків. У правильно організованому ресторані кожен займається власною справою: кухар — кухнею, кур’єр — доставкою, бухгалтер — фінансами.
Класичний антипатерн у Java:
// ПОГАНО: Один клас поєднує 4 різні відповідальності
public class Invoice {
private Long id;
private BigDecimal amount;
// 1. Бізнес-логіка розрахунку податку
public BigDecimal calculateTax() {
return amount.multiply(new BigDecimal("0.20"));
}
// 2. Логіка персистентності (SQL / JDBC)
public void saveToDatabase() {
String sql = "INSERT INTO invoices (id, amount) VALUES (?, ?)";
// Відкриття Connection, PreparedStatement, executeUpdate...
}
// 3. Форматування представлення (HTML / PDF)
public String generatePdfReport() {
return "<html><body><h1>Рахунок #" + id + "</h1></body></html>";
}
// 4. Комунікація / Сповіщення
public void emailToCustomer(String email) {
// Налаштування SMTP сесії, Transport.send...
}
}
🟡 Middle Level
1. Реальні приклади порушення SRP у Spring Boot
Приклад 1: «Всеїдний» Spring @Service
Найпоширеніший антипатерн в enterprise-розробці — перетворення сервісу на безформне сміттєзвалище з 15 інжектованих репозиторіїв:
// ПОГАНО: GeneralUserService знає про все на світі
@Service
@RequiredArgsConstructor
public class GeneralUserService {
private final UserRepository userRepo;
private final OrderRepository orderRepo;
private final EmailClient emailClient;
private final PasswordEncoder encoder;
private final S3Client s3Client;
private final AuditLogger auditLogger;
// Причина 1: Змінилися правила валідації паролів (безпека)
public void registerUser(UserDto dto) { /* ... */ }
// Причина 2: Змінився провайдер хмарного сховища S3 (DevOps)
public void uploadAvatar(Long userId, byte[] image) { /* ... */ }
// Причина 3: Змінився шаблон листа для скидання пароля (маркетинг)
public void sendPasswordResetEmail(String email) { /* ... */ }
// Причина 4: Бухгалтерія змінила формат експорту історії замовлень (фінанси)
public byte[] exportOrderHistoryToExcel(Long userId) { /* ... */ }
}
Приклад 2: Поєднання JPA Entity та DTO в одному класі
// ПОГАНО: Сутність бази даних одночасно є DTO для REST API
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
@Column(name = "db_password_hash")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // Деталі JSON-серіалізації!
private String password;
@Transient
@Pattern(regexp = "^\\+?[0-9]{10,13}$") // Валідація вхідного HTTP-запиту!
private String rawPhoneInput;
}
У чому помилка: Зміна контракту публічного фронтенду неминуче зачіпає структуру таблиці в PostgreSQL, і навпаки.
🔴 Senior Level
1. Руйнівні наслідки порушення SRP
Порушення SRP
├──► Крихкість (Fragility): Виправлення логіки відправки email ламає розрахунок знижок
├──► Нерухомість (Immobility): Неможливо перевикористати метод без затягування 15 чужих залежностей
├──► Конфлікти злиття (Merge Conflicts): 10 розробників одночасно правлять один God-клас
└──► Вичерпання транзакційних ресурсів (Connection Starvation):
Небезпека Connection Starvation при порушенні SRP:
Якщо в одному транзакційному методі сервісу поєднано взаємодію з БД та повільний зовнішній HTTP-виклик:
@Transactional // Захоплює з'єднання з БД із пулу HikariCP!
public void processOrder(OrderDto dto) {
Order order = orderRepo.save(new Order(dto)); // 1. Швидкий INSERT (2 мс)
// 2. Виклик зовнішнього сервісу еквайрингу ПО МЕРЕЖІ всередині транзакції!
// Якщо банк гальмує 5 секунд — потік усі 5 секунд утримує з'єднання з БД!
bankPaymentGateway.charge(dto.getCard(), dto.getAmount());
order.setStatus(OrderStatus.PAID);
orderRepo.save(order);
}
При 100 паралельних запитах пул з’єднань бази даних (maximum-pool-size = 30) миттєво вичерпується. Уся база даних паралізована через те, що транзакційний метод порушив SRP і поєднав персистентність із повільним мережевим I/O.
2. Рефакторинг: Decorator замість роздування сервісу
Як додати до сервісу кешування, аудит та збір метрик без порушення SRP?
- ❌ Погано: Впровадити
MeterRegistryтаCacheManagerбезпосередньо вOrderService(клас починає відповідати за бізнес-логіку + моніторинг + кеш). - ✅ Добре: Використати патерн Decorator (Декоратор):
// 1. Чистий контракт
public interface OrderService {
Order createOrder(OrderRequest req);
}
// 2. Лише чиста бізнес-логіка (SRP)
@Service
@Primary
public class CoreOrderService implements OrderService {
@Override
public Order createOrder(OrderRequest req) {
// Лише створення та перевірка бізнес-правил замовлення
return new Order(req);
}
}
// 3. Декоратор метрик: відповідає ВИКЛЮЧНО за телеметрію (SRP)
@Service
@RequiredArgsConstructor
public class TimedOrderServiceDecorator implements OrderService {
private final OrderService delegate;
private final MeterRegistry registry;
@Override
public Order createOrder(OrderRequest req) {
return registry.timer("order.creation.time")
.record(() -> delegate.createOrder(req));
}
}
4 Tricky Questions
1. Як поєднання @Entity (JPA) та @JsonProperty (Jackson) в одному класі порушує SRP і до яких критичних проблем це призводить?
Відповідь: Клас бере на себе дві принципово різні ролі:
- Відображення на реляційну схему БД (Domain/Data model) — змінюється під час міграцій СУБД, оптимізації індексів, зв’язків
@OneToMany. - Публічний контракт зовнішнього API (Presentation/Transport model) — змінюється відповідно до вимог фронтенду чи мобільних клієнтів.
Проблеми на практиці:
- Витік конфіденційних даних (Over-fetching / Security Leak): Випадково додане в Entity поле
passwordHashабоinternalAuditIdавтоматично витікає в JSON-відповідь клієнту, якщо розробник забув вказати@JsonIgnore. - Циклічна рекурсія: Двоспрямовані зв’язки (
@ManyToOneта@OneToMany) призводять до фатальногоStackOverflowErrorпід час серіалізації в JSON. Рішення: Суворий поділ наOrderEntity(для бази даних) таOrderResponseDto(для REST API) з мапінгом через MapStruct.
- Витік конфіденційних даних (Over-fetching / Security Leak): Випадково додане в Entity поле
2. Що таке Characterization Tests (тести фіксації поведінки / Golden Master) і як вони застосовуються під час розпилу God Object?
Відповідь: Під час рефакторингу застарілого «божественного класу» на 5000 рядків модульні тести зазвичай повністю відсутні. Писати класичні Unit-тести важко, оскільки код заплутаний і містить численні приховані побічні ефекти. Методика Golden Master:
- Класу передається репрезентативний набір різноманітних вхідних параметрів (наприклад, 1000 різних запитів).
- Вихідні дані (стан бази даних, згенеровані файли, повернені структури) фіксуються у файлі-еталоні (Golden Master).
- Створюється автотест, який перевіряє, що за будь-яких внутрішніх перетворень код повертає точно зафіксований еталонний результат.
- Під надійним прикриттям Golden Master розробники безпечно екстрагують методи в окремі SRP-класи.
3. Чому в архітектурі CQRS поділ сутності на OrderCommandService та OrderQueryService вважається торжеством принципу SRP?
Відповідь: Читання (Query) та запис (Command) мають принципово різні вимоги:
- Запис: Вимагає суворих бізнес-інваріантів, блокувань, транзакційної цілісності, нормалізації даних і валідації. Замовник вимог — експерти доменної області.
- Читання: Вимагає максимальної швидкості, денормалізації, кешування, складних агрегацій та пагінації для UI. Замовник — UI/UX дизайнери та клієнтські застосунки. Поєднання їх в одному класі змушує його змінюватися як при додаванні фільтра на екрані каталогу, так і при зміні податкового законодавства. Поділ на Command і Query надає кожному сервісу рівно одну причину для зміни.
4. У чому різниця між Cohesion (внутрішньою зв’язністю) та Coupling (зовнішнім зачепленням) на прикладі класу, що порушує SRP?
Відповідь:
- Cohesion (Внутрішня зв’язність): Показує, наскільки елементи всередині одного класу сфокусовані на досягненні єдиної мети. У класі-порушнику SRP внутрішня зв’язність є екстремально низькою (Low Cohesion): метод
calculateSalary()жодним чином не пов’язаний із методомsendEmail(), вони оперують різними полями й не мають спільної задачі. - Coupling (Зовнішнє зачеплення): Показує, наскільки сильно клас залежить від інших компонентів системи. У порушника SRP зачеплення є екстремально високим (High Coupling): йому потрібні десятки зовнішніх залежностей (
UserRepository,EmailSender,S3Client,PdfGenerator), і збій будь-якої з них паралізує весь клас.
🎯 Шпаргалка для інтерв’ю
Головне:
- Порушення SRP виникає, коли клас виконує декілька різнорідних функцій і змінюється на вимогу різних стейкхолдерів.
- Типові порушники: God Object, сервіси на 1000+ рядків, Entity з Jackson-анотаціями, методи зі змішуванням SQL та HTTP.
- Найнебезпечніший наслідок у Spring: Connection Pool Starvation, якщо всередині методу з
@Transactionalвиконується повільний виклик стороннього API через мережу. - Для наскрізної функціональності (логування, кешування, метрики) використовуйте Decorator або Spring AOP, а не роздувайте бізнес-сервіс.
- Завжди розділяйте Entity (база) та DTO (API).
- Ознаки порушення: конструктор із 10+ залежностями, у тестах понад 5 моків, заплутані
if-elseна всі випадки життя.
Червоні прапорці (Чого говорити НЕ МОЖНА):
- ❌ «Поєднувати Entity і DTO в одному класі — це нормальна оптимізація, щоб не створювати зайві файли» (Призводить до витоку даних, StackOverflowError і жорсткої прив’язки бази до API).
- ❌ «У методі з
@Transactionalнормально робити виклики сторонніх платіжних шлюзів по HTTP» (Блокує з’єднання з БД і призводить до колапсу пулу з’єднань). - ❌ «Якщо клас має 20 методів, він автоматично порушує SRP» (Якщо всі 20 методів сфокусовані на одній меті, наприклад
BigDecimal, SRP не порушено).