Паттерны проектирования в TypeScript: когда они спасают проект, а когда мешают

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

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

Начать курс

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

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

В этой статье мы не будем пересказывать сухую теорию из книг GoF (Банды четырёх). Вместо этого разберём, какие паттерны проектирования TypeScript реально нужны в современной разработке, когда они спасают проект, а когда только добавляют головной боли. Поехали!

Почему TypeScript меняет правила игры?

Прежде чем нырять в паттерны, давайте вспомним главную фишку TypeScript — статическую типизацию. В JavaScript мы часто использовали паттерны, чтобы компенсировать динамическую природу языка. Например, Фабричный метод (Factory Method) помогал скрыть сложность создания объектов. В TypeScript эта сложность часто решается просто интерфейсами и дженериками.

Но это не значит, что паттерны умерли. Наоборот, TypeScript делает их более выразительными и безопасными. Компилятор проверяет, что вы правильно реализовали структуру, и не даёт сделать глупую ошибку. Вот где TypeScript раскрывает силу паттернов GoF.

Топ-5 паттернов, которые вы будете использовать чаще всего

1. Singleton (Одиночка) — когда один — хорошо, а два — лишние

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

Зачем в TypeScript: TypeScript позволяет сделать Singleton потокобезопасным и типобезопасным. Без него вы рискуете плодить десятки объектов, которые будут тратить память и путать состояние.

Пример кода:

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}`);
    }
}

// Использование
const db = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');
db.query('SELECT * FROM users');

// Попытка создать ещё одно подключение вернёт тот же экземпляр
const db2 = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');
console.log(db === db2); // true

Антипаттерн: Не делайте Singleton ради Singleton. Если объект может существовать в нескольких экземплярах с разными состояниями (например, разные подключения к разным БД), то Singleton — зло.

2. 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}`);
    }
}

// Фабрика
class NotificationFactory {
    createNotification(type: 'email' | 'sms'): Notification {
        switch (type) {
            case 'email':
                return new EmailNotification();
            case 'sms':
                return new SMSNotification();
            default:
                throw new Error('Неизвестный тип уведомления');
        }
    }
}

// Использование
const factory = new NotificationFactory();
const notification = factory.createNotification('email');
notification.send('Ваш заказ подтверждён!');

Совет: Если у вас всего два типа, фабрика может быть избыточной. Но как только типов становится больше трёх, паттерн Factory Method окупается сторицей.

3. Observer (Наблюдатель) — реактивность на коленке

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

Зачем в TypeScript: TypeScript позволяет строго типизировать события, что снижает количество багов. В JavaScript вы часто ловили ошибки "событие не обработано", здесь компилятор подскажет.

Пример кода:

// Тип события
interface Event {
    type: string;
    data: unknown;
}

// Наблюдатель
type Observer = (event: Event) => void;

// Субъект
class EventBus {
    private observers: Observer[] = [];

    subscribe(observer: Observer): void {
        this.observers.push(observer);
    }

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

    emit(event: Event): void {
        this.observers.forEach(observer => observer(event));
    }
}

// Использование
const bus = new EventBus();

const logObserver: Observer = (event) => {
    console.log(`Событие: ${event.type}`, event.data);
};

bus.subscribe(logObserver);
bus.emit({ type: 'USER_LOGIN', data: { userId: 1 } });
// Вывод: Событие: USER_LOGIN { userId: 1 }

Важно: Не злоупотребляйте Observer. Если у вас сотни подписчиков, производительность может упасть. И не забывайте отписываться, чтобы избежать утечек памяти.

4. Strategy (Стратегия) — алгоритмы на замену

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

Зачем в TypeScript: Стратегия превращает раздутые if-else цепочки в чистый, расширяемый код. Типизация делает каждую стратегию предсказуемой.

Пример кода:

// Интерфейс стратегии
interface DiscountStrategy {
    calculate(price: number): number;
}

// Конкретные стратегии
class NoDiscount implements DiscountStrategy {
    calculate(price: number): number {
        return price;
    }
}

class PercentageDiscount implements DiscountStrategy {
    constructor(private percent: number) {}

    calculate(price: number): number {
        return price * (1 - this.percent / 100);
    }
}

class FixedDiscount implements DiscountStrategy {
    constructor(private amount: number) {}

    calculate(price: number): number {
        return Math.max(0, price - this.amount);
    }
}

// Контекст
class ShoppingCart {
    private strategy: DiscountStrategy;

    constructor(strategy: DiscountStrategy) {
        this.strategy = strategy;
    }

    setStrategy(strategy                

Блоги

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