Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке принцип єдиної відповідальності (SRP)?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Single Responsibility Principle (SRP)** - клас повинен мати тільки одну причину для зміни. Розділяй за причиною: `User` зберігає дані, `UserRepository` відповідає за БД, `EmailService` за email. Збій БД не повинен змушувати тебе відкривати файл з email-логікою. **Головне:** одна причина для змін, а не один метод на клас.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Single Responsibility Principle (SRP)** - клас повинен мати тільки одну причину для зміни. Кожен клас відповідає за одну конкретну річ. ## Теорія ### TL;DR - Аналогія: ресторанна кухня - кухар готує, офіціант подає, посудомийник миє. Змішай ролі і будь-яка зміна зламає все разом. - SRP розділяє код за *причиною* зміни, а не за кількістю методів. - Клас змінюється з двох непов'язаних причин? Розділи на два. - Правило: якщо збій БД і збій email обидва змушують відкрити один файл - клас порушує SRP. ### Швидкий приклад ```javascript // ПОГАНО: User змінюється і через БД, і через email - дві причини class User { constructor(name, email) { this.name = name; this.email = email; } save() { db.insert(this); } // причина 1: БД sendEmail() { mailer.send(this); } // причина 2: email } // ДОБРЕ: кожен клас змінюється з однієї причини class User { constructor(name, email) { this.name = name; this.email = email; } } class UserRepository { save(user) { db.insert(user); } } class EmailService { send(user) { mailer.send(user.email); } } ``` Після розділення збій email-сервісу не зачіпає `UserRepository`. Тести залишаються ізольованими. ### Головна різниця SRP - про те, *чому* змінюється клас, а не про кількість методів. Клас `User` з 10 гетерами для імені, email і адреси повністю відповідає SRP - він змінюється тільки коли змінюються правила даних користувача. Додаєш `save()` - вже дві причини. Тепер зміна схеми БД змушує відкривати файл з логікою даних, що не має до неї жодного стосунку. ### Коли застосовувати - Клас перевалив за 50 рядків і продовжує рости. Витягуй за причиною зміни, а не за кількістю рядків. - Два незалежних підрозділи (команда API, команда звітів) редагують один клас. Розділяй на межі їх відповідальності. - Тестовий набір одного класу покриває непов'язані сценарії. Кожна група тестів - окремий клас. - Хтось у команді каже "цей клас робить усе". Перевір `git blame`, знайди дві найчастіші причини змін і розділи. ### Як це влаштовано Жодної магії в рантаймі. SRP - це правило дизайну, яке застосовуєш на code review і під час рефакторингу. Але є практичний бонус: V8 краще оптимізує менші, сфокусовані класи. Великі god classes можуть спричиняти промахи в inline cache (megamorphic IC) на критичних шляхах виконання, що сповільнює роботу. Тобто розділення - це не тільки про читабельність. ### Типові помилки **Помилка: "мало методів = SRP".** ```javascript class User { getName() { return this.name; } getEmail() { return this.email; } save() { db.insert(this); } // порушує SRP - друга причина для змін } ``` Три методи, дві причини для змін. Винеси `save()` до `UserRepository`. Проблема зникає. **Помилка: god object заради зручності.** З того, що я бачив, це майже завжди починається з "потім приберу". Приховані залежності між відповідальностями ростуть, поки міграція БД не зламає email-тест і ніхто не розуміє чому. Перевір `git log` - якщо один клас з'являється в непов'язаних комітах, розділяй. **Помилка: TypeScript інтерфейс зі змішаними відповідальностями.** ```typescript // Виглядає чисто, але порушує SRP interface UserService extends UserRepository, Emailer {} ``` TypeScript дозволяє таке структурно. Lint не зловить. Але коли зміниться БД, ти редагуєш інтерфейс, який також описує поведінку email. **Помилка: надмірне дроблення на нано-класи.** SRP не означає один метод на клас. Якщо два методи завжди змінюються разом з однієї причини, вони мають бути в одному класі. Надмірне дроблення додає зайву непряму залежність без користі. ### Де зустрічається - **Express.js:** тонкі обробники маршрутів делегують до `UserRepository` і `EmailService` окремо. Контролери не торкаються логіки БД напряму. - **React:** компоненти тільки рендерять. Завантаження даних іде в кастомний хук або HOC. Аналітика - в `useTrack`. Кожна частина змінюється незалежно. - **NestJS:** `@Controller`, `@Injectable` сервіс і репозиторій - три окремих класи за конвенцією. Фреймворк сам веде до SRP. - **Prisma:** клієнт Prisma обробляє запити. Бізнес-логіка живе в окремому шарі сервісів. Їх не мішають. ### Питання на співбесіді **Q:** Чим SRP відрізняється від "один метод на клас"? **A:** SRP групує за причиною зміни, а не за кількістю методів. Клас з 10 гетерами для даних користувача повністю відповідає SRP. Додати `save()` - вже порушення, бо зміна схеми БД тепер вимагає правки файлу, де нічого БД-шного немає. **Q:** Як рефакторити legacy-клас, що порушує SRP, без повного переписування? **A:** Патерн Strangler. Витягуй одну відповідальність за раз за feature flag. Починай з тих, що змінюються найчастіше - `git log` по імені класу покаже де тиск найбільший. **Q:** Які компроміси SRP в мікросервісній архітектурі? **A:** Більше класів, більше файлів, більше залежностей для з'єднання. Але кожен сервіс масштабується і деплоїться незалежно. Міра: чи змінюються непов'язані речі в одному файлі. Якщо ні - SRP працює. **Q:** Як перевірити SRP тестами? **A:** Один набір тестів на клас. Якщо тести для `User` мокають і БД, і email-клієнт, клас робить занадто багато. ## Приклади ### Базовий: розділення класу User ```javascript // Три відповідальності в одному класі: дані, збереження, email class BadUser { constructor(name, email) { this.name = name; this.email = email; } save() { db.insert(this); } // змінюється через БД sendWelcome() { mailer.send(this.email); } // змінюється через email } // Після розділення за SRP class User { constructor(name, email) { this.name = name; this.email = email; } } class UserRepository { save(user) { db.insert(user); } // змінюється тільки через БД } class WelcomeEmailService { send(user) { mailer.send(user.email); } // змінюється тільки через email } const user = new User('Alice', 'alice@example.com'); new UserRepository().save(user); // "Saved to DB" new WelcomeEmailService().send(user); // "Email sent" ``` Збій БД тепер стосується тільки `UserRepository`. Email-тести не ламаються. Ось у чому практична перевага розділення. ### Середній рівень: Express-обробник маршруту (продакшен-патерн) ```javascript // ПОГАНО: один обробник мішає валідацію, БД та email app.post('/users', (req, res) => { const user = req.body; // без валідації db.users.insert(user); // логіка БД email.sendWelcome(user.email); // логіка email res.json({ success: true }); }); // ДОБРЕ: обробник делегує до сфокусованих класів class UserValidator { static validate(data) { if (!data.email) throw new Error('Email required'); return data; } } class UserRepository { async save(data) { return db.users.create(data); } } class WelcomeEmailService { async send(email) { /* логіка Nodemailer */ } } app.post('/users', async (req, res) => { const validated = UserValidator.validate(req.body); // кидає помилку при невалідних даних await new UserRepository().save(validated); await new WelcomeEmailService().send(validated.email); res.json({ success: true }); }); ``` Якщо БД переходить з Postgres на MongoDB, змінюється тільки `UserRepository`. Тести валідації і email продовжують проходити. Кожен клас має рівно одну причину, щоб його відкрити.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.