Паттерны проектирования в TypeScript: от простого к сложному
Паттерны проектирования (design patterns) — это проверенные временем решения типовых задач в разработке программного обеспечения. В контексте TypeScript, который добавляет в JavaScript строгую типизацию и возможности ООП, паттерны становятся особенно эффективными. TypeScript позволяет реализовать абстракции, интерфейсы и полиморфизм на новом уровне, делая код более предсказуемым и безопасным.
В этой статье мы разберем ключевые паттерны проектирования, их реализацию на TypeScript и практические примеры использования. Материал будет полезен как начинающим разработчикам, так и опытным специалистам, желающим систематизировать знания.
Порождающие паттерны: Singleton и Factory Method
Порождающие паттерны отвечают за создание объектов, скрывая логику инстанцирования. Рассмотрим два самых популярных представителя: Singleton и Factory Method.
Singleton (Одиночка)
Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. В TypeScript реализация становится особенно элегантной благодаря статическим членам и приватным конструкторам.
class DatabaseConnection {
private static instance: DatabaseConnection;
private connectionString: string;
private constructor(connectionString: string) {
this.connectionString = connectionString;
console.log(`Создано соединение с: ${connectionString}`);
}
static getInstance(connectionString: string): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection(connectionString);
}
return DatabaseConnection.instance;
}
query(sql: string): void {
console.log(`Выполнение запроса: ${sql} на ${this.connectionString}`);
}
}
// Использование
const db1 = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');
const db2 = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');
console.log(db1 === db2); // true - один и тот же объект
Этот паттерн идеально подходит для управления ресурсами: пулы соединений, логирование, кэширование. Однако будьте осторожны — Singleton может усложнить тестирование из-за глобального состояния.
Factory Method (Фабричный метод)
Factory Method определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта. В TypeScript это реализуется через абстрактные классы или интерфейсы.
interface Notification {
send(message: string): void;
}
class EmailNotification implements Notification {
send(message: string): void {
console.log(`Отправка email: ${message}`);
}
}
class SMSNotification implements Notification {
send(message: string): void {
console.log(`Отправка SMS: ${message}`);
}
}
abstract class NotificationFactory {
abstract createNotification(): Notification;
notify(message: string): void {
const notification = this.createNotification();
notification.send(message);
}
}
class EmailFactory extends NotificationFactory {
createNotification(): Notification {
return new EmailNotification();
}
}
class SMSFactory extends NotificationFactory {
createNotification(): Notification {
return new SMSNotification();
}
}
// Использование
const factory: NotificationFactory = new EmailFactory();
factory.notify('Ваш заказ подтвержден!');
Factory Method помогает соблюдать принцип открытости/закрытости (OCP) — вы можете добавлять новые типы уведомлений без изменения существующего кода.
Структурные паттерны: Adapter и Decorator
Структурные паттерны помогают организовать связи между объектами и классами, делая систему более гибкой и расширяемой.
Adapter (Адаптер)
Adapter позволяет объектам с несовместимыми интерфейсами работать вместе. В TypeScript это часто используется при интеграции сторонних библиотек или работе с устаревшим кодом.
interface ModernAPI {
request(): string;
}
class LegacySystem {
specificRequest(): string {
return 'Legacy data format';
}
}
class LegacyAdapter implements ModernAPI {
private legacySystem: LegacySystem;
constructor(legacySystem: LegacySystem) {
this.legacySystem = legacySystem;
}
request(): string {
const result = this.legacySystem.specificRequest();
return `Адаптировано: ${result}`;
}
}
// Использование
const legacy = new LegacySystem();
const adapter = new LegacyAdapter(legacy);
console.log(adapter.request()); // "Адаптировано: Legacy data format"
Adapter — это «мостик» между старым и новым кодом. Он позволяет постепенно мигрировать систему без полного рефакторинга.
Decorator (Декоратор)
Decorator динамически добавляет объекту новые обязанности. В TypeScript декораторы — это встроенная языковая возможность (экспериментальная), но мы рассмотрим классическую реализацию паттерна.
interface Coffee {
cost(): number;
description(): string;
}
class SimpleCoffee implements Coffee {
cost(): number {
return 10;
}
description(): string {
return 'Обычный кофе';
}
}
class MilkDecorator implements Coffee {
private coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
cost(): number {
return this.coffee.cost() + 3;
}
description(): string {
return `${this.coffee.description()}, с молоком`;
}
}
class SugarDecorator implements Coffee {
private coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
cost(): number {
return this.coffee.cost() + 2;
}
description(): string {
return `${this.coffee.description()}, с сахаром`;
}
}
// Использование
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()}: ${coffee.cost()} руб.`);
// "Обычный кофе, с молоком, с сахаром: 15 руб."
Decorator — мощный инструмент для создания гибких систем, где функциональность можно комбинировать как конструктор Lego.
Поведенческие паттерны: Observer и Strategy
Поведенческие паттерны определяют способы взаимодействия между объектами, распределяя обязанности и алгоритмы.
Observer (Наблюдатель)
Observer создает механизм подписки, позволяющий одним объектам следить за изменениями в других. TypeScript с его строгой типизацией делает реализацию особенно надежной.
interface Observer {
update(subject: Subject): void;
}
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}
class WeatherStation implements Subject {
private observers: Observer[] = [];
private temperature: number = 0;
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(): void {
for (const observer of this.observers) {
observer.update(this);
}
}
setTemperature(temp: number): void {
this.temperature = temp;
this.notify();
}
getTemperature(): number {
return this