SOLID принципы в TypeScript: теория и практика с примерами

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

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

Начать курс

Введение: Почему SOLID важен в TypeScript

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

В этой статье мы разберем каждый из пяти принципов SOLID с точки зрения TypeScript, сопроводив теорию практическими примерами кода. Вы узнаете, как применять эти принципы для улучшения архитектуры ваших проектов.

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

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

Нарушение принципа:

// ❌ Плохо: класс отвечает и за данные, и за сохранение, и за форматирование
class User {
  constructor(public name: string, public email: string) {}

  saveToDatabase(): void {
    console.log(`Сохранение ${this.name} в БД`);
  }

  generateHTML(): string {
    return `<div>${this.name} - ${this.email}</div>`;
  }
}

Исправление с применением SRP:

// ✅ Хорошо: разделяем ответственность на три класса
class User {
  constructor(public name: string, public email: string) {}
}

class UserRepository {
  save(user: User): void {
    console.log(`Сохранение ${user.name} в БД`);
  }
}

class UserView {
  render(user: User): string {
    return `<div>${user.name} - ${user.email}</div>`;
  }
}

Теперь каждый класс имеет одну четкую обязанность, что упрощает тестирование и изменение кода.

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

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

Нарушение принципа:

// ❌ Плохо: при добавлении нового типа фигуры нужно менять класс AreaCalculator
class AreaCalculator {
  calculate(shape: any): number {
    if (shape.type === 'circle') {
      return Math.PI * shape.radius ** 2;
    } else if (shape.type === 'rectangle') {
      return shape.width * shape.height;
    }
    return 0;
  }
}

Исправление с применением OCP:

// ✅ Хорошо: используем интерфейс и полиморфизм
interface Shape {
  area(): number;
}

class Circle implements Shape {
  constructor(public radius: number) {}
  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

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

class AreaCalculator {
  calculate(shape: Shape): number {
    return shape.area(); // Закрыто для модификации, открыто для расширения
  }
}

Теперь для добавления новой фигуры (например, Triangle) достаточно создать новый класс, реализующий интерфейс Shape — без изменения AreaCalculator.

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

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

Нарушение принципа:

// ❌ Плохо: наследник нарушает контракт базового класса
class Bird {
  fly(): string {
    return 'Летит';
  }
}

class Penguin extends Bird {
  fly(): string {
    throw new Error('Пингвины не летают!'); // Нарушение LSP
  }
}

function makeBirdFly(bird: Bird): void {
  console.log(bird.fly()); // Упадет с ошибкой для Penguin
}

Исправление с применением LSP:

// ✅ Хорошо: разделяем иерархию
interface Bird {
  move(): string;
}

class FlyingBird implements Bird {
  move(): string {
    return 'Летит';
  }
}

class Penguin implements Bird {
  move(): string {
    return 'Плавает';
  }
}

function makeBirdMove(bird: Bird): void {
  console.log(bird.move()); // Работает для всех птиц
}

Соблюдение LSP гарантирует, что ваш код остается предсказуемым и безопасным при использовании полиморфизма.

4. Interface Segregation Principle (Принцип разделения интерфейса)

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

Нарушение принципа:

// ❌ Плохо: толстый интерфейс, который заставляет реализовывать ненужные методы
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

class Robot 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 Human implements Workable, Eatable, Sleepable {
  work(): void { console.log('Человек работает'); }
  eat(): void { console.log('Человек ест'); }
  sleep(): void { console.log('Человек спит'); }
}

class Robot implements Workable {
  work(): void { console.log('Робот работает'); }
  // Не нужно реализовывать eat() и sleep()
}

Теперь каждый класс реализует только те интерфейсы, которые ему действительно нужны.

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

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

Нарушение принци

Блоги

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