Перейти к содержимому

2. SOLID: Single Responsibility

SOLID — это аббревиатура из пяти принципов, которые делают объектно-ориентированный код гибким и поддерживаемым. Автор — Роберт Мартин, популяризировал — Майкл Фезерс.

SRP (Single Responsibility Principle) — Принцип единственной ответственности.

Класс должен иметь только одну причину для изменения.


Если у класса есть несколько причин для изменения — он делает слишком много. Представь отдел в компании: бухгалтерия считает деньги, HR нанимает людей. Если один человек отвечает и за бухгалтерию, и за HR — это проблема. Любое изменение в одной области затрагивает другую.

// ❌ Нарушение SRP — класс делает три вещи:
// 1. Хранит данные пользователя
// 2. Валидирует данные
// 3. Сохраняет в базу данных
class User {
name: string;
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
validate(): boolean {
return this.name.length > 0 && this.email.includes('@');
}
saveToDatabase(): void {
// SQL запрос прямо в классе пользователя?!
db.query(`INSERT INTO users (name, email) VALUES ('${this.name}', '${this.email}')`);
}
sendWelcomeEmail(): void {
emailService.send(this.email, 'Добро пожаловать!');
}
}

Почему это плохо? Если изменится структура базы данных — меняем класс User. Если изменится логика валидации — меняем класс User. Если изменится шаблон email — снова меняем User. Три разные причины для изменения одного класса.


Правильный подход: разделение ответственностей

Заголовок раздела «Правильный подход: разделение ответственностей»
// ✅ Каждый класс имеет одну ответственность
// Модель данных — только данные
class User {
constructor(
public readonly name: string,
public readonly email: string,
) {}
}
// Валидация — только валидация
class UserValidator {
validate(user: User): ValidationResult {
const errors: string[] = [];
if (!user.name || user.name.length < 2) {
errors.push('Name must be at least 2 characters');
}
if (!user.email || !user.email.includes('@')) {
errors.push('Email is invalid');
}
return { isValid: errors.length === 0, errors };
}
}
// Репозиторий — только работа с БД
class UserRepository {
async save(user: User): Promise<void> {
await db.query(
'INSERT INTO users (name, email) VALUES ($1, $2)',
[user.name, user.email]
);
}
async findById(id: string): Promise<User | null> {
const row = await db.query('SELECT * FROM users WHERE id = $1', [id]);
return row ? new User(row.name, row.email) : null;
}
}
// Email сервис — только отправка писем
class UserNotificationService {
async sendWelcomeEmail(user: User): Promise<void> {
await emailService.send({
to: user.email,
subject: 'Добро пожаловать!',
template: 'welcome',
data: { name: user.name },
});
}
}
// Сервис — оркестрация
class UserRegistrationService {
constructor(
private validator: UserValidator,
private repository: UserRepository,
private notifications: UserNotificationService,
) {}
async register(name: string, email: string): Promise<User> {
const user = new User(name, email);
const validation = this.validator.validate(user);
if (!validation.isValid) {
throw new ValidationError(validation.errors);
}
await this.repository.save(user);
await this.notifications.sendWelcomeEmail(user);
return user;
}
}

Как понять, что класс нарушает SRP:

  1. Название включает «И»: UserManagerAndEmailSender, DataFetcherAndProcessor
  2. Класс > 200-300 строк — вероятно, слишком много обязанностей
  3. Изменения в одной части ломают другую — они не должны быть связаны
  4. Тесты приходится писать для очень разных сценариев в одном тест-файле
  5. Комментарии типа «// --- Email section ---» — явное разделение ответственностей внутри одного класса

Распространённое заблуждение: SRP = один метод на класс. Нет. Класс может иметь много методов — главное чтобы все они служили одной цели.

// Все методы UserRepository служат одной цели — работе с данными пользователей
class UserRepository {
async findById(id: string): Promise<User> { ... }
async findByEmail(email: string): Promise<User> { ... }
async save(user: User): Promise<void> { ... }
async update(user: User): Promise<void> { ... }
async delete(id: string): Promise<void> { ... }
async findAll(filter?: UserFilter): Promise<User[]> { ... }
}

Это нормально — все методы про «хранение и получение пользователей».


// ❌ Плагин-монстр который делает всё
class MyPlugin {
public function register_post_type() { ... }
public function send_notification_email() { ... }
public function process_payment() { ... }
public function generate_pdf() { ... }
public function sync_with_crm() { ... }
}
// ✅ Разделённые классы
class PostTypeRegistrar {
public function register() { ... }
}
class NotificationService {
public function sendEmail(User $user, string $message): void { ... }
}
class PaymentProcessor {
public function process(Order $order): PaymentResult { ... }
}

SRP — не оправдание для создания 100 крошечных классов. Чрезмерное дробление тоже вредит:

  • Если поведение всегда меняется вместе — держи его вместе
  • Не создавай отдельный класс для одной строки кода
  • Ориентируйся на бизнес-логику, а не на технические детали

  1. Найди в своём проекте самый большой класс/файл. Перечисли его ответственности. Можно ли разделить?

  2. Рефактори следующий класс согласно SRP:

class BlogPost {
title: string;
content: string;
authorId: string;
renderToHtml(): string { /* конвертация в HTML */ }
saveToDatabase(): void { /* SQL запрос */ }
generateSlug(): string { /* создание URL slug */ }
countWords(): number { /* подсчёт слов */ }
sendToRSSFeed(): void { /* публикация в RSS */ }
}
  1. Создай систему обработки заказов с разделёнными классами: Order, OrderValidator, OrderRepository, OrderNotificationService, OrderService.

  2. Напиши unit-тест для UserValidator из примера выше. Обрати внимание насколько легко тестировать изолированный класс.

  3. Прочитай статью о SRP на Refactoring Guru и найди ещё 2 признака нарушения принципа.