SOLID принципы в TypeScript: продвинутый уровень с примерами

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

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

Начать курс

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

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

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

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

Продвинутая практика: Часто нарушение SRP проявляется в "божественных объектах" (God Objects). Используйте композицию и делегирование.

// ❌ Нарушение SRP: класс отвечает и за данные, и за валидацию, и за сохранение
class UserService {
  constructor(private user: User) {}

  validateEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  saveToDatabase(): void {
    // логика сохранения
  }

  sendWelcomeEmail(): void {
    // логика отправки
  }
}

// ✅ SRP: разделяем ответственность
interface IEmailValidator {
  isValid(email: string): boolean;
}

interface IUserRepository {
  save(user: User): Promise<void>;
}

interface INotificationService {
  sendWelcome(user: User): Promise<void>;
}

class UserRegistrationService {
  constructor(
    private validator: IEmailValidator,
    private repository: IUserRepository,
    private notifier: INotificationService
  ) {}

  async register(user: User): Promise<void> {
    if (!this.validator.isValid(user.email)) {
      throw new Error('Invalid email');
    }
    await this.repository.save(user);
    await this.notifier.sendWelcome(user);
  }
}

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

Теория: Классы должны быть открыты для расширения, но закрыты для модификации. В TypeScript это достигается через абстрактные классы и интерфейсы.

Продвинутая практика: Используйте паттерн "Стратегия" (Strategy) для реализации OCP.

// ❌ Нарушение OCP: при добавлении нового типа отчета нужно менять класс
class ReportGenerator {
  generate(data: any[], type: string): string {
    if (type === 'csv') {
      return data.map(row => row.join(',')).join('\n');
    } else if (type === 'json') {
      return JSON.stringify(data);
    }
    // при добавлении 'xml' — модифицируем этот метод
    throw new Error('Unsupported format');
  }
}

// ✅ OCP: используем полиморфизм
interface IReportFormatter {
  format(data: any[]): string;
}

class CsvFormatter implements IReportFormatter {
  format(data: any[]): string {
    return data.map(row => row.join(',')).join('\n');
  }
}

class JsonFormatter implements IReportFormatter {
  format(data: any[]): string {
    return JSON.stringify(data);
  }
}

class XmlFormatter implements IReportFormatter {
  format(data: any[]): string {
    // реализация для XML
    return '<data>...</data>';
  }
}

class ReportGeneratorOCP {
  constructor(private formatter: IReportFormatter) {}

  generate(data: any[]): string {
    return this.formatter.format(data);
  }
}

// Использование: расширяем без модификации
const csvReport = new ReportGeneratorOCP(new CsvFormatter());
const jsonReport = new ReportGeneratorOCP(new JsonFormatter());

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

Теория: Объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения корректности программы. В TypeScript это особенно критично из-за номинативной типизации.

Продвинутая практика: Избегайте нарушения контрактов базового класса. Используйте шаблон "Шаблонный метод" (Template Method).

// ❌ Нарушение LSP: подкласс усиливает предусловия
abstract class Bird {
  abstract fly(): void;
}

class Sparrow extends Bird {
  fly(): void {
    console.log('Sparrow flying');
  }
}

class Penguin extends Bird {
  fly(): void {
    throw new Error('Penguins cannot fly!'); // ❌ Нарушение контракта
  }
}

// ✅ LSP: корректная иерархия
interface IFlyable {
  fly(): void;
}

interface ISwimmable {
  swim(): void;
}

abstract class BirdLSP {
  abstract eat(): void;
}

class SparrowLSP extends BirdLSP implements IFlyable {
  eat(): void { /* ... */ }
  fly(): void { console.log('Sparrow flying'); }
}

class PenguinLSP extends BirdLSP implements ISwimmable {
  eat(): void { /* ... */ }
  swim(): void { console.log('Penguin swimming'); }
}

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

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

Продвинутая практика: Используйте discriminated unions и utility types для гибких интерфейсов.

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

class HumanWorker implements Worker {
  work(): void { /* ... */ }
  eat(): void { /* ... */ }
  sleep(): void { /* ... */ }
}

class RobotWorker implements Worker {
  work(): void { /* ... */ }
  eat(): void { throw new Error('Robots don\'t eat'); } // ❌
  sleep(): void { throw new Error('Robots don\'t sleep'); } // ❌
}

// ✅ ISP: разделяем интерфейсы
interface IWorkable {
  work(): void;
}

interface IEatable {
  eat(): void;
}

interface ISleepable {
  sleep(): void;
}

type HumanCapabilities = IWorkable & IEatable & ISleepable;
type RobotCapabilities = IWorkable;

class HumanWorkerISP implements HumanCapabilities {
  work(): void { /* ... */ }
  eat(): void { /* ... */ }
  sleep(): void { /* ... */ }
}

class RobotWorkerISP implements RobotCapabilities {
  work(): void { /* ... */ }
}

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

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

Продвинутая практика: Используйте DI-контейнеры (например, InversifyJS) или простую фабрику для инверсии.

// ❌ Нарушение DIP:                

Блоги

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