Пользуешься ли принципом Single responsibility
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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;
}
}
Проблемы:
- Если меняется логика email'ов, нужно менять UserService
- Если меняется БД, нужно менять UserService
- Если меняется логирование, нужно менять UserService
- Сложно тестировать (нужно мокировать БД, email, logger, cache одновременно)
- Сложно переиспользовать части (нельзя взять только логирование)
Решение: Разделяем Ответственность
Правильный подход:
// 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'е.