← Назад к вопросам

Пользуешься ли принципом Single responsibility

2.0 Middle🔥 111 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Single Responsibility Principle (SRP) в Разработке

Да, я активно использую принцип Single Responsibility Principle (SRP) — это один из самых важных принципов SOLID архитектуры. Для меня это не просто красивая идея, а практическая необходимость для написания масштабируемого и поддерживаемого кода.

Что такое Single Responsibility?

Определение: класс или функция должна иметь одну, и только одну, причину для изменения.

Другими словами:

  • Одна ответственность = одна причина для изменения
  • Если ты не можешь описать, что делает класс, в одном предложении — у него слишком много ответственности

Проблемы без SRP

Пример: UserService без SRP

// ❌ Плохо — слишком много ответственности
class UserService {
  // Ответственность 1: управление пользователями
  async createUser(data: CreateUserDTO) {
    const user = new User(data);
    await this.database.save(user);
    return user;
  }

  // Ответственность 2: отправка email'ов
  async sendWelcomeEmail(email: string) {
    const emailContent = `<html>...
    </html>`; // HTML генерируется здесь
    await this.emailService.send(email, emailContent);
  }

  // Ответственность 3: логирование
  async logUserActivity(userId: string, action: string) {
    const timestamp = new Date();
    await this.logger.info(`User ${userId} performed ${action} at ${timestamp}`);
  }

  // Ответственность 4: кеширование
  private cache = new Map();
  async getUserWithCache(userId: string) {
    if (this.cache.has(userId)) {
      return this.cache.get(userId);
    }
    const user = await this.database.findById(userId);
    this.cache.set(userId, user);
    return user;
  }
}

Проблемы:

  1. Если меняется логика email'ов, нужно менять UserService
  2. Если меняется БД, нужно менять UserService
  3. Если меняется логирование, нужно менять UserService
  4. Сложно тестировать (нужно мокировать БД, email, logger, cache одновременно)
  5. Сложно переиспользовать части (нельзя взять только логирование)

Решение: Разделяем Ответственность

Правильный подход:

// 1. Только управление пользователями
export class UserRepository {
  async create(data: CreateUserDTO): Promise<User> {
    const user = new User(data);
    await this.database.save(user);
    return user;
  }

  async findById(id: string): Promise<User | null> {
    return await this.database.findById(id);
  }
}

// 2. Только отправка email'ов
export class EmailService {
  async sendWelcomeEmail(email: string, userName: string): Promise<void> {
    const template = this.emailTemplateProvider.getWelcomeTemplate(userName);
    await this.smtpProvider.send(email, template);
  }
}

// 3. Только email templates
export class EmailTemplateProvider {
  getWelcomeTemplate(userName: string): string {
    return `
      <h1>Welcome, ${userName}!</h1>
      <p>Thank you for joining...</p>
    `;
  }
}

// 4. Только логирование
export class Logger {
  info(message: string): void {
    console.log(`[INFO] ${new Date().toISOString()} - ${message}`);
  }
}

// 5. Только кеширование
export class CacheService<T> {
  private cache = new Map<string, T>();
  
  get(key: string): T | undefined {
    return this.cache.get(key);
  }
  
  set(key: string, value: T): void {
    this.cache.set(key, value);
  }
}

// 6. Оркестратор — координирует всё (Use Case)
export class CreateUserUseCase {
  constructor(
    private userRepo: UserRepository,
    private emailService: EmailService,
    private logger: Logger,
  ) {}

  async execute(data: CreateUserDTO): Promise<User> {
    // Координируем работу других сервисов
    const user = await this.userRepo.create(data);
    this.logger.info(`User created: ${user.id}`);
    
    await this.emailService.sendWelcomeEmail(user.email, user.name);
    this.logger.info(`Welcome email sent to ${user.email}`);
    
    return user;
  }
}

Преимущества этого подхода

1. Легко тестировать

describe('UserRepository', () => {
  it('should create user', async () => {
    // Не нужно мокировать email, logger, cache
    const repo = new UserRepository(mockDatabase);
    const user = await repo.create({ name: 'John', email: 'john@example.com' });
    expect(user.id).toBeDefined();
  });
});

describe('EmailService', () => {
  it('should send welcome email', async () => {
    // Не нужно мокировать БД, logger, cache
    const service = new EmailService(mockTemplateProvider, mockSmtp);
    await service.sendWelcomeEmail('john@example.com', 'John');
    expect(mockSmtp.send).toHaveBeenCalled();
  });
});

2. Легко изменять Если нужно изменить способ отправки email (SMTP → SendGrid):

// Просто создаём новый EmailService с новым провайдером
const emailService = new EmailService(
  new SendgridSmtpProvider(), // Новый провайдер
  emailTemplateProvider,
);
// Остальной код не меняется

3. Переиспользуемость

// EmailService можно использовать везде
class NotificationService {
  constructor(private emailService: EmailService) {}
  
  async notifyUser(userId: string) {
    const user = await this.userRepo.findById(userId);
    await this.emailService.sendWelcomeEmail(user.email, user.name);
  }
}

4. Понятный код Когда каждый класс делает одно — код легче понять.

SRP в Разных Слоях Архитектуры

В Domain Layer

// Только бизнес-логика
export class User {
  private email: string;
  private password: string;
  
  constructor(email: string, password: string) {
    if (!email.includes('@')) throw new Error('Invalid email');
    this.email = email;
    this.password = password;
  }
  
  getEmail(): string { return this.email; }
}

В Application Layer

// Координация Use Case'ов
export class UserApplicationService {
  async createUser(dto: CreateUserDTO): Promise<void> {
    // Использует domain objects и infrastructure
  }
}

В Infrastructure Layer

// Только технические детали
export class PostgresUserRepository implements UserRepository {
  async create(user: User): Promise<void> {
    const query = 'INSERT INTO users (email, password) VALUES ($1, $2)';
    await this.db.query(query, [user.getEmail(), user.getPassword()]);
  }
}

Когда Нарушать SRP

Не всегда SRP имеет смысл. Например:

// Здесь нормально объединить — это гетт и сеттер
class Point {
  constructor(public x: number, public y: number) {}
}

// А не создавать:
class XCoordinate { constructor(public value: number) {} }
class YCoordinate { constructor(public value: number) {} }
class Point { constructor(x: XCoordinate, y: YCoordinate) {} }

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

Практический Пример из Real Project

Задача: создать API для регистрации пользователя с отправкой email и логированием.

Структура папок:

src/
├── domain/
│   └── user/
│       └── User.ts          # Бизнес-логика
├── application/
│   └── use-cases/
│       └── CreateUserUseCase.ts
├── infrastructure/
│   ├── repositories/
│   │   └── PostgresUserRepository.ts
│   ├── services/
│   │   ├── EmailService.ts
│   │   ├── Logger.ts
│   │   └── CacheService.ts
│   └── templates/
│       └── EmailTemplates.ts
└── presentation/
    └── controllers/
        └── UserController.ts

UserController.ts:

export class UserController {
  async createUser(req: Request, res: Response) {
    try {
      const user = await this.createUserUseCase.execute(req.body);
      res.status(201).json(user);
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }
}

CreateUserUseCase.ts:

export class CreateUserUseCase {
  async execute(dto: CreateUserDTO): Promise<User> {
    const user = User.create(dto);
    await this.userRepository.save(user);
    await this.emailService.sendWelcomeEmail(user.email);
    this.logger.info(`User ${user.id} created`);
    return user;
  }
}

Заключение

Single Responsibility Principle — это не просто красивая архитектура, это:

  • Проще тестировать — не нужны сложные моки
  • Проще менять — изменения локализованы
  • Проще читать — каждый класс делает одно
  • Проще переиспользовать — компоненты независимы

Это принцип, который я применяю в каждом новом проекте и всегда ищу нарушения SRP при code review'е.