Паттерны проектирования в TypeScript: примеры и применение

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

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

Начать курс

Паттерны проектирования в TypeScript: практическое руководство

Паттерны проектирования (design patterns) — это проверенные решения типовых задач в разработке программного обеспечения. В TypeScript, благодаря строгой типизации и возможностям ООП, многие паттерны реализуются особенно элегантно и безопасно. В этой статье мы разберем четыре ключевых паттерна с реальными примерами кода, которые помогут вам писать более гибкий и поддерживаемый код.

1. Singleton (Одиночка)

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

Реализация с использованием приватного конструктора и статического метода:

class DatabaseConnection {
  private static instance: DatabaseConnection;
  private connection: string;

  private constructor() {
    this.connection = 'PostgreSQL connected';
  }

  public static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }

  public query(sql: string): void {
    console.log(`Executing query: ${sql} on ${this.connection}`);
  }
}

// Использование
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - один и тот же объект
db1.query('SELECT * FROM users');

Когда использовать: для логгеров, кэшей, пулов потоков, настроек приложения. Избегайте Singleton для состояний, которые могут меняться в многопоточной среде.

2. Factory Method (Фабричный метод)

Factory Method определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта. В TypeScript это отлично сочетается с интерфейсами и абстрактными классами.

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

interface Notification {
  send(message: string): void;
}

class EmailNotification implements Notification {
  public send(message: string): void {
    console.log(`Email sent: ${message}`);
  }
}

class SMSNotification implements Notification {
  public send(message: string): void {
    console.log(`SMS sent: ${message}`);
  }
}

abstract class NotificationFactory {
  public abstract createNotification(): Notification;

  public notifyUser(message: string): void {
    const notification = this.createNotification();
    notification.send(message);
  }
}

class EmailNotificationFactory extends NotificationFactory {
  public createNotification(): Notification {
    return new EmailNotification();
  }
}

class SMSNotificationFactory extends NotificationFactory {
  public createNotification(): Notification {
    return new SMSNotification();
  }
}

// Использование
const factory: NotificationFactory = new EmailNotificationFactory();
factory.notifyUser('Your order has been shipped!');

Когда использовать: когда заранее неизвестно, какой конкретный класс понадобится, или когда логика создания объекта сложная и должна быть централизована.

3. Observer (Наблюдатель)

Observer определяет отношение "один ко многим" между объектами: при изменении состояния одного объекта все зависимые объекты уведомляются автоматически. В TypeScript это часто реализуется через дженерики для типизации событий.

Пример системы подписок на новости:

interface Observer<T> {
  update(data: T): void;
}

class NewsPublisher<T> {
  private observers: Observer<T>[] = [];

  public subscribe(observer: Observer<T>): void {
    this.observers.push(observer);
  }

  public unsubscribe(observer: Observer<T>): void {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  public notify(data: T): void {
    this.observers.forEach(observer => observer.update(data));
  }
}

class User implements Observer<string> {
  constructor(private name: string) {}

  public update(news: string): void {
    console.log(`${this.name} received news: ${news}`);
  }
}

// Использование
const publisher = new NewsPublisher<string>();
const user1 = new User('Alice');
const user2 = new User('Bob');

publisher.subscribe(user1);
publisher.subscribe(user2);
publisher.notify('New TypeScript 5.0 released!');
// Alice received news: New TypeScript 5.0 released!
// Bob received news: New TypeScript 5.0 released!

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

4. Decorator (Декоратор)

Decorator позволяет динамически добавлять новое поведение объектам, оборачивая их в классы-декораторы. TypeScript имеет встроенную поддержку декораторов (экспериментальная), но мы рассмотрим классический паттерн без них.

Пример расширения функциональности кофе:

interface Coffee {
  cost(): number;
  description(): string;
}

class SimpleCoffee implements Coffee {
  public cost(): number {
    return 5;
  }

  public description(): string {
    return 'Simple coffee';
  }
}

class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  public cost(): number {
    return this.coffee.cost() + 2;
  }

  public description(): string {
    return this.coffee.description() + ', with milk';
  }
}

class SugarDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  public cost(): number {
    return this.coffee.cost() + 1;
  }

  public description(): string {
    return this.coffee.description() + ', with sugar';
  }
}

// Использование
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()} costs $${coffee.cost()}`);
// Simple coffee, with milk, with sugar costs $8

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

Заключение

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

Советы по применению:

  • Не злоупотребляйте Singleton — он может усложнить тестирование и создать скрытые зависимости.
  • Factory Method незаменим, когда нужно отделить код создания объектов от бизнес-логики.
  • Observer отлично подходит для событийно-ориентированной архитектуры и реактивных систем.
  • Decorator помогает избежать "взрыва классов" при комбинировании функциональности.

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

Блоги

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