Паттерны проектирования в TypeScript: от теории к практике
Паттерны проектирования (Design Patterns) — это проверенные решения типовых задач в разработке программного обеспечения. В контексте TypeScript, с его мощной системой типов и поддержкой ООП, многие классические паттерны из книги "Банды четырёх" (GoF) получают новое, более элегантное воплощение. В этой статье мы разберём три ключевых паттерна с реальными примерами на TypeScript, которые помогут вам писать более чистый, масштабируемый и поддерживаемый код.
1. Порождающий паттерн: Singleton (Одиночка)
Назначение: Гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.
В TypeScript реализация Singleton особенно удобна благодаря статическим свойствам и приватным конструкторам. Этот паттерн идеален для управления подключениями к базе данных, логгерами или кэшами.
class DatabaseConnection {
private static instance: DatabaseConnection;
private connectionString: string;
private constructor(connectionString: string) {
this.connectionString = connectionString;
console.log(`Создано подключение к: ${connectionString}`);
}
public static getInstance(connectionString?: string): DatabaseConnection {
if (!DatabaseConnection.instance) {
if (!connectionString) {
throw new Error("Connection string required for first initialization");
}
DatabaseConnection.instance = new DatabaseConnection(connectionString);
}
return DatabaseConnection.instance;
}
public query(sql: string): void {
console.log(`Выполнение запроса: ${sql} на ${this.connectionString}`);
}
}
// Использование
const db1 = DatabaseConnection.getInstance("postgres://localhost:5432/mydb");
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true — один и тот же объект
db1.query("SELECT * FROM users");
Важно: В TypeScript можно использовать readonly и дженерики для более гибких реализаций. Однако помните, что Singleton может усложнить тестирование — рассмотрите внедрение зависимостей (DI) как альтернативу.
2. Структурный паттерн: Decorator (Декоратор)
Назначение: Динамически добавляет объекту новые обязанности, не изменяя его структуру. В TypeScript декораторы — это первоклассная возможность языка (экспериментальная в ES5, но активно используется в Angular и NestJS).
Рассмотрим пример с декоратором для логирования вызовов методов:
// Целевой интерфейс
interface ILogger {
log(message: string): void;
}
// Базовый класс
class SimpleLogger implements ILogger {
public log(message: string): void {
console.log(`[LOG]: ${message}`);
}
}
// Декоратор (класс-обёртка)
class TimestampDecorator implements ILogger {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
public log(message: string): void {
const timestamp = new Date().toISOString();
this.logger.log(`[${timestamp}] ${message}`);
}
}
// Декоратор для форматирования
class UpperCaseDecorator implements ILogger {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
public log(message: string): void {
this.logger.log(message.toUpperCase());
}
}
// Использование
let logger: ILogger = new SimpleLogger();
logger.log("Привет мир"); // [LOG]: Привет мир
logger = new TimestampDecorator(logger);
logger = new UpperCaseDecorator(logger);
logger.log("декораторы работают");
// [LOG]: [2025-01-15T10:30:00.000Z] ДЕКОРАТОРЫ РАБОТАЮТ
Совет: В TypeScript также доступны декораторы классов и свойств (синтаксис @decorator). Они активно используются в Angular для внедрения зависимостей и метаданных, но требуют включения experimentalDecorators в tsconfig.json.
3. Поведенческий паттерн: Observer (Наблюдатель)
Назначение: Определяет зависимость "один ко многим" между объектами, так что при изменении состояния одного объекта все зависящие от него уведомляются и обновляются автоматически.
В TypeScript этот паттерн отлично ложится на систему событий (EventEmitter) и RxJS. Рассмотрим простую реализацию:
// Интерфейс наблюдателя
interface Observer<T> {
update(data: T): void;
}
// Субъект (издатель)
class Subject<T> {
private observers: Observer<T>[] = [];
private state: T;
public attach(observer: Observer<T>): void {
const isExist = this.observers.includes(observer);
if (!isExist) {
this.observers.push(observer);
}
}
public detach(observer: Observer<T>): void {
this.observers = this.observers.filter(obs => obs !== observer);
}
public notify(): void {
for (const observer of this.observers) {
observer.update(this.state);
}
}
public setState(newState: T): void {
this.state = newState;
console.log(`Состояние изменилось на: ${newState}`);
this.notify();
}
}
// Конкретный наблюдатель
class UserNotifier implements Observer<string> {
private name: string;
constructor(name: string) {
this.name = name;
}
public update(data: string): void {
console.log(`[${this.name}] Получено обновление: ${data}`);
}
}
// Использование
const newsFeed = new Subject<string>();
const user1 = new UserNotifier("Алиса");
const user2 = new UserNotifier("Боб");
newsFeed.attach(user1);
newsFeed.attach(user2);
newsFeed.setState("Новая статья: Паттерны в TypeScript");
// Вывод:
// Состояние изменилось на: Новая статья: Паттерны в TypeScript
// [Алиса] Получено обновление: Новая статья: Паттерны в TypeScript
// [Боб] Получено обновление: Новая статья: Паттерны в TypeScript
newsFeed.detach(user1);
newsFeed.setState("Обновление цен на курсы");
// Боб получит уведомление, Алиса — нет
Практика: В реальных проектах на TypeScript (особенно в Angular) Observer паттерн реализуется через Subject из RxJS. Это даёт мощные возможности для работы с асинхронными потоками данных.
4. Паттерн Strategy (Стратегия) — гибкая замена switch-case
Назначение: Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Паттерн позволяет изменять поведение объекта во время выполнения.
// Интерфейс стратегии
interface PaymentStrategy {
pay(amount: number): void;
}
// Конкретные стратегии
class CreditCardPayment