Архитектурные паттерны в TypeScript: как перестать писать "спагетти" и начать жить
Вы когда-нибудь открывали проект на TypeScript, написанный полгода назад, и с ужасом понимали, что не можете разобраться в собственных классах? Или, может быть, вы добавляли новую фичу, а приложение падало в трёх неожиданных местах? Знакомо? Добро пожаловать в клуб разработчиков, которые однажды решили, что "и так сойдёт" — и поплатились за это.
TypeScript — это не просто надстройка над JavaScript с типами. Это мощный инструмент, который позволяет строить сложные, масштабируемые и поддерживаемые системы. Но типы сами по себе не спасут вас от архитектурного хаоса. Нужны архитектурные паттерны — проверенные временем рецепты, которые помогут организовать код так, чтобы он был понятным, тестируемым и гибким.
В этой статье мы разберём пять ключевых архитектурных паттернов TypeScript, которые чаще всего используются в реальных проектах. Вы увидите примеры кода, поймёте, где и зачем применять каждый паттерн, и сможете сразу внедрить их в свою работу. Поехали!
1. MVC (Model-View-Controller) — классика жанра
Начнём с самого известного паттерна. MVC в TypeScript — это разделение приложения на три слоя:
- Model — данные и бизнес-логика.
- View — отображение (UI).
- Controller — связующее звено, обрабатывает ввод пользователя.
В TypeScript приложениях MVC особенно хорош тем, что мы можем строго типизировать все слои, исключая ошибки на этапе компиляции.
Пример: простой список задач (Todo List)
// Model
interface Task {
id: number;
title: string;
completed: boolean;
}
class TaskModel {
private tasks: Task[] = [];
addTask(title: string): Task {
const newTask: Task = {
id: Date.now(),
title,
completed: false,
};
this.tasks.push(newTask);
return newTask;
}
getTasks(): Task[] {
return this.tasks;
}
}
// View
class TaskView {
render(tasks: Task[]): void {
console.clear();
tasks.forEach(task => {
console.log(`${task.id}: [${task.completed ? 'x' : ' '}] ${task.title}`);
});
}
}
// Controller
class TaskController {
constructor(
private model: TaskModel,
private view: TaskView
) {}
addTask(title: string): void {
this.model.addTask(title);
this.view.render(this.model.getTasks());
}
showAll(): void {
this.view.render(this.model.getTasks());
}
}
// Использование
const model = new TaskModel();
const view = new TaskView();
const controller = new TaskController(model, view);
controller.addTask('Изучить паттерны TypeScript');
controller.addTask('Написать статью');
Почему это круто? Вы можете менять View (например, перейти с консоли на React), не трогая Model. Можете тестировать бизнес-логику отдельно от UI. Архитектура TypeScript приложения становится прозрачной.
2. Observer — король событий
Если ваше приложение живёт за счёт событий (клики, ответы сервера, таймеры), паттерн Observer TypeScript — ваш лучший друг. Он позволяет одним объектам (подписчикам) следить за изменениями другого объекта (издателя).
Пример: система уведомлений
// Интерфейс наблюдателя
interface Observer {
update(data: string): void;
}
// Издатель
class NewsPublisher {
private observers: Observer[] = [];
subscribe(observer: Observer): void {
this.observers.push(observer);
}
unsubscribe(observer: Observer): void {
this.observers = this.observers.filter(obs => obs !== observer);
}
publishNews(news: string): void {
this.observers.forEach(obs => obs.update(news));
}
}
// Конкретный наблюдатель: Email-уведомление
class EmailNotifier implements Observer {
update(data: string): void {
console.log(`📧 Email отправлен: ${data}`);
}
}
// Конкретный наблюдатель: SMS-уведомление
class SMSNotifier implements Observer {
update(data: string): void {
console.log(`📱 SMS отправлено: ${data}`);
}
}
// Использование
const publisher = new NewsPublisher();
const emailNotifier = new EmailNotifier();
const smsNotifier = new SMSNotifier();
publisher.subscribe(emailNotifier);
publisher.subscribe(smsNotifier);
publisher.publishNews('TypeScript 5.0 вышел!');
// 📧 Email отправлен: TypeScript 5.0 вышел!
// 📱 SMS отправлено: TypeScript 5.0 вышел!
Observer паттерн TypeScript идеально ложится на систему типов: мы чётко определяем, какие данные передаются. Это основа реактивного программирования.
3. Factory — когда создание объектов — это сложно
Иногда создание объекта — не просто new ClassName(). Нужно подгрузить конфигурацию, выбрать реализацию, проверить зависимости. Тут на помощь приходит Factory паттерн TypeScript.
Пример: создание подключения к базе данных
// Абстрактный продукт
interface DatabaseConnection {
connect(): void;
query(sql: string): any[];
}
// Конкретные продукты
class MySQLConnection implements DatabaseConnection {
connect(): void { console.log('Подключение к MySQL'); }
query(sql: string): any[] { return []; }
}
class PostgreSQLConnection implements DatabaseConnection {
connect(): void { console.log('Подключение к PostgreSQL'); }
query(sql: string): any[] { return []; }
}
// Фабрика
class DatabaseFactory {
static createDatabase(type: 'mysql' | 'postgres'): DatabaseConnection {
switch (type) {
case 'mysql':
return new MySQLConnection();
case 'postgres':
return new PostgreSQLConnection();
default:
throw new Error('Неподдерживаемая БД');
}
}
}
// Использование
const db = DatabaseFactory.createDatabase('postgres');
db.connect(); // Подключение к PostgreSQL
Этот архитектурный паттерн TypeScript скрывает сложность создания объектов и делает код гибким. Хотите добавить MongoDB? Просто расширяете фабрику.
4. Repository — прослойка между бизнес-логикой и данными
Repository паттерн TypeScript — это абстракция над хранилищем данных. Вы работаете с коллекциями объектов, не думая, откуда они берутся: из базы, API или кэша.
Пример: работа с пользователями
// Сущность
interface User {
id: number;
name: string;
email: string;
}
// Интерфейс репозитория
interface UserRepository {
findById(id: number): User | null;
save(user: User): void;
delete(id: number): void;
}
// Реализация: работа с массивом в памяти
class InMemoryUserRepository implements UserRepository {
private users: User[] = [];
findById(id: number): User | null {
return this.users.find(u => u.id === id) || null;
}
save(user: User): void {
const index = this.users.findIndex(u => u.id === user.id);
if (index !== -1) {
this.users[index] = user;
} else {