SOLID принципы в TypeScript: теория и практика от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему SOLID важен для TypeScript разработчика

SOLID — это пять фундаментальных принципов объектно-ориентированного программирования, сформулированных Робертом Мартином (дядюшкой Бобом). В контексте TypeScript, который сочетает статическую типизацию и гибкость JavaScript, применение SOLID принципов позволяет создавать масштабируемые, тестируемые и легко поддерживаемые приложения. В этой статье мы разберем каждый принцип с теоретической базой и практическими примерами на TypeScript.

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

Теория

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

Практика на TypeScript

Рассмотрим нарушение SRP на примере класса, который управляет пользователями и отправкой уведомлений:

// ❌ Нарушение SRP
class UserManager {
  constructor(private name: string, private email: string) {}

  saveUser(): void {
    console.log(`Пользователь ${this.name} сохранен в БД`);
  }

  sendWelcomeEmail(): void {
    console.log(`Приветственное письмо отправлено на ${this.email}`);
  }
}

Исправленный вариант с разделением ответственности:

// ✅ Соблюдение SRP
class User {
  constructor(public name: string, public email: string) {}
}

class UserRepository {
  save(user: User): void {
    console.log(`Пользователь ${user.name} сохранен в БД`);
  }
}

class EmailService {
  sendWelcomeEmail(user: User): void {
    console.log(`Приветственное письмо отправлено на ${user.email}`);
  }
}

2. Open/Closed Principle (OCP) — Принцип открытости/закрытости

Теория

Программные сущности должны быть открыты для расширения, но закрыты для модификации. Другими словами, вы должны иметь возможность добавлять новую функциональность без изменения существующего кода.

Практика на TypeScript

Пример с расчетом скидок для разных типов клиентов:

// ❌ Нарушение OCP
class DiscountCalculator {
  calculateDiscount(customerType: string, amount: number): number {
    if (customerType === 'regular') {
      return amount * 0.1;
    } else if (customerType === 'premium') {
      return amount * 0.2;
    } else if (customerType === 'vip') {
      return amount * 0.3;
    }
    return 0;
  }
}
// ✅ Соблюдение OCP через полиморфизм
interface DiscountStrategy {
  calculate(amount: number): number;
}

class RegularDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.1;
  }
}

class PremiumDiscount implements DiscountStrategy {
  calculate(amount: number): number {
    return amount * 0.2;
  }
}

class DiscountCalculator {
  constructor(private strategy: DiscountStrategy) {}

  calculate(amount: number): number {
    return this.strategy.calculate(amount);
  }
}

3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Теория

Объекты в программе должны быть заменяемы экземплярами их подтипов без изменения корректности программы. Простыми словами: если у вас есть базовый класс, любой его наследник должен корректно работать везде, где используется базовый класс.

Практика на TypeScript

Классический пример с геометрическими фигурами:

// ❌ Нарушение LSP
class Rectangle {
  constructor(protected width: number, protected height: number) {}

  setWidth(width: number): void {
    this.width = width;
  }

  setHeight(height: number): void {
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width: number): void {
    this.width = width;
    this.height = width; // Нарушение!
  }

  setHeight(height: number): void {
    this.height = height;
    this.width = height; // Нарушение!
  }
}
// ✅ Соблюдение LSP через абстракцию
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 Principle (ISP) — Принцип разделения интерфейсов

Теория

Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько узкоспециализированных интерфейсов, чем один общий.

Практика на TypeScript

// ❌ Нарушение ISP — "толстый" интерфейс
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

class HumanWorker implements Worker {
  work(): void { console.log('Работаю'); }
  eat(): void { console.log('Ем'); }
  sleep(): void { console.log('Сплю'); }
}

class RobotWorker implements Worker {
  work(): void { console.log('Работаю'); }
  eat(): void { throw new Error('Роботы не едят'); }
  sleep(): void { throw new Error('Роботы не спят'); }
}
// ✅ Соблюдение ISP
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

class HumanWorker implements Workable, Eatable, Sleepable {
  work(): void { console.log('Работаю'); }
  eat(): void { console.log('Ем'); }
  sleep(): void { console.log('Сплю'); }
}

class RobotWorker implements Workable {
  work(): void { console.log('Работаю'); }
}

5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Теория

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей — детали должны зависеть от абстракций.

Практика на TypeScript

// ❌ Нарушение DIP
class MySQLDatabase {
  save(data: string): void {
    console.log(`Сохранено в MySQL: ${data}`);
  }
}

class UserService {
  private database: MySQLDatabase;

  constructor() {
    this.database = new MySQLDatabase(); // Жесткая связь
  }

  saveUser(user: string): void {
    this.database.save(user);
  }
}
// ✅ Соблюдение DIP
interface Database {
  save(data: string): void;
}

class MySQLDatabase implements Database {
  save(data: string): void {
    console.log(`Сохранено в MySQL: ${data}`);
  }
}

class PostgreSQLDatabase implements Database {
  save(data                

Блоги

Рекомендаций по книгам