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

Зачем нужен SOLID?

1.0 Junior🔥 231 комментариев
#Архитектура и паттерны

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

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

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

Зачем нужен SOLID?

Что это такое?

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

Single Responsibility
Open/Closed
Liskov Substitution
Interface Segregation
Dependency Inversion

1. Single Responsibility (Единственная ответственность)

Идея: Класс должен отвечать за одно и только одно.

// ❌ Плохо: класс делает слишком много
class User {
  save(): void { /* сохранение в БД */ }
  sendEmail(): void { /* отправка email */ }
  generateReport(): void { /* генерация отчёта */ }
}

// ✅ Хорошо: каждый класс отвечает за одно
class User {
  // Только данные пользователя
}

class UserRepository {
  save(user: User): void { /* сохранение в БД */ }
}

class EmailService {
  send(to: string, message: string): void { /* email */ }
}

class ReportGenerator {
  generate(user: User): string { /* отчёт */ }
}

Преимущества:

  • Легче тестировать (одна ответственность = простой тест)
  • Проще изменять (нужна правка в email? только EmailService)
  • Переиспользуемость (ReportGenerator используется везде)

2. Open/Closed (Открыто для расширения, закрыто для изменения)

Идея: Можно добавлять новую функциональность, но не нужно менять существующий код.

// ❌ Плохо: нужно менять класс при добавлении нового типа
class PaymentProcessor {
  process(type: string, amount: number): void {
    if (type === 'credit_card') {
      // обработка кредитной карты
    } else if (type === 'paypal') {
      // обработка PayPal
    } else if (type === 'crypto') {
      // обработка крипто
    }
  }
}

// ✅ Хорошо: новые платежи добавляются без изменения ProcessPayment
interface PaymentMethod {
  process(amount: number): Promise<void>;
}

class CreditCardPayment implements PaymentMethod {
  async process(amount: number): Promise<void> { /* ... */ }
}

class PayPalPayment implements PaymentMethod {
  async process(amount: number): Promise<void> { /* ... */ }
}

class CryptoPayment implements PaymentMethod {
  async process(amount: number): Promise<void> { /* ... */ }
}

class PaymentProcessor {
  async process(method: PaymentMethod, amount: number): Promise<void> {
    await method.process(amount);
  }
}

Преимущества:

  • Минимальный риск регрессии (не трогаешь старый код)
  • Легче добавлять новые функции
  • Масштабируемость (в будущем добавить BitcoinPayment легко)

3. Liskov Substitution (Принцип подстановки Лисков)

Идея: Подклассы могут заменять своих родителей без нарушения корректности.

// ❌ Плохо: Square не может корректно быть Rectangle
class Rectangle {
  width: number = 0;
  height: number = 0;

  setWidth(w: number): void { this.width = w; }
  setHeight(h: number): void { this.height = h; }
  getArea(): number { return this.width * this.height; }
}

class Square extends Rectangle {
  setWidth(w: number): void {
    this.width = w;
    this.height = w; // Квадрат нарушает логику Rectangle
  }
  setHeight(h: number): void {
    this.height = h;
    this.width = h;
  }
}

// Код сломается
const rect: Rectangle = new Square();
rect.setWidth(5);
rect.setHeight(10);
console.log(rect.getArea()); // Ожидаем 50, получим 100!

// ✅ Хорошо: разные интерфейсы для разных фигур
interface Shape {
  getArea(): number;
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
  getArea(): number { return this.width * this.height; }
}

class Square implements Shape {
  constructor(private side: number) {}
  getArea(): number { return this.side * this.side; }
}

4. Interface Segregation (Разделение интерфейса)

Идея: Не заставляй класс реализовывать методы, которые ему не нужны.

// ❌ Плохо: Worker должен реализовать и работу, и питание
interface Worker {
  work(): void;
  eat(): void;
}

class Robot implements Worker {
  work(): void { /* робот работает */ }
  eat(): void { /* робот не ест! ошибка */ }
}

// ✅ Хорошо: разные интерфейсы для разных ответственностей
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

class Robot implements Workable {
  work(): void { /* робот работает */ }
}

class Human implements Workable, Eatable {
  work(): void { /* человек работает */ }
  eat(): void { /* человек ест */ }
}

5. Dependency Inversion (Инверсия зависимостей)

Идея: Зависьте от абстракций, не от конкретных реализаций.

// ❌ Плохо: UserService зависит от конкретной БД
class PostgreSQLDatabase {
  save(data: any): void { /* ... */ }
}

class UserService {
  constructor(private db: PostgreSQLDatabase) {}
  createUser(user: User): void {
    this.db.save(user);
  }
}

// Если нужно переключиться на MongoDB, весь код ломается!

// ✅ Хорошо: зависимость от интерфейса
interface Database {
  save(data: any): void;
}

class PostgreSQL implements Database {
  save(data: any): void { /* PostgreSQL */ }
}

class MongoDB implements Database {
  save(data: any): void { /* MongoDB */ }
}

class UserService {
  constructor(private db: Database) {} // Зависит от интерфейса!
  createUser(user: User): void {
    this.db.save(user);
  }
}

// Легко переключиться между БД
const db: Database = new PostgreSQL();
const service = new UserService(db);

Практическая польза

1. Поддерживаемость

// С SOLID кодом проще найти нужное место
// Логика email → EmailService
// Логика БД → Repository
// Логика бизнеса → UseCase/Service

2. Тестируемость

class UserService {
  constructor(private repo: UserRepository) {}
}

// В тестах подменяем настоящий репозиторий на mock
const mockRepo = { findById: jest.fn() };
const service = new UserService(mockRepo as any);

3. Масштабируемость

// Не нужно переписывать старый код при добавлении новых платежей
// Просто добавляем новый класс, реализующий интерфейс

Реальный пример из backend

// SOLID архитектура для создания пользователя
interface UserRepository {
  save(user: User): Promise<void>;
}

interface NotificationService {
  sendWelcomeEmail(email: string): Promise<void>;
}

class CreateUserUseCase {
  constructor(
    private userRepo: UserRepository,
    private notificationService: NotificationService
  ) {}

  async execute(userData: CreateUserDTO): Promise<User> {
    const user = new User(userData);
    await this.userRepo.save(user); // S - один класс за сохранение
    await this.notificationService.sendWelcomeEmail(user.email); // S - отдельно
    return user;
  }
}

// Легко тестировать, легко добавлять новые провайдеры notifications

Выводы

SOLID нужен, чтобы:

  • Код был проще поддерживать — изменения локализованы
  • Легче тестировать — каждая часть независима
  • Масштабируться — добавлять функционал без переписывания
  • Переиспользовать код — компоненты слабо связаны
  • Ловить баги раньше — четкие контракты (интерфейсы)

Это инвестиция времени в начале, но окупается при поддержке и развитии проекта.

Зачем нужен SOLID? | PrepBro