Паттерны проектирования в 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 предоставляет для этого все необходимые возможности.