Паттерны проектирования в TypeScript: продвинутый уровень с примерами
Разработка сложных приложений на TypeScript требует не только знания синтаксиса, но и умения строить гибкую, масштабируемую архитектуру. Паттерны проектирования GoF (Gang of Four) остаются золотым стандартом, но на продвинутом уровне их применение становится более тонким. В этой статье мы разберем четыре мощных паттерна — Bridge, Visitor, State и Strategy — с реальными примерами на TypeScript. Вы узнаете, как использовать строгую типизацию, дженерики и интерфейсы для реализации этих паттернов.
1. Bridge: отделяем абстракцию от реализации
Паттерн Bridge (Мост) разделяет абстракцию и её реализацию, позволяя изменять их независимо. Это особенно полезно, когда у вас есть несколько вариаций функциональности, которые могут комбинироваться.
Пример: Система рендеринга фигур в разных графических API (Canvas и SVG).
// Реализация (Implementation)
interface Renderer {
renderCircle(radius: number): void;
renderSquare(side: number): void;
}
class CanvasRenderer implements Renderer {
renderCircle(radius: number): void {
console.log(`Canvas: рисуем круг радиусом ${radius}`);
}
renderSquare(side: number): void {
console.log(`Canvas: рисуем квадрат со стороной ${side}`);
}
}
class SVGRenderer implements Renderer {
renderCircle(radius: number): void {
console.log(`SVG: <circle r="${radius}" />`);
}
renderSquare(side: number): void {
console.log(`SVG: <rect width="${side}" height="${side}" />`);
}
}
// Абстракция (Abstraction)
abstract class Shape {
protected renderer: Renderer;
constructor(renderer: Renderer) {
this.renderer = renderer;
}
abstract draw(): void;
}
class Circle extends Shape {
private radius: number;
constructor(renderer: Renderer, radius: number) {
super(renderer);
this.radius = radius;
}
draw(): void {
this.renderer.renderCircle(this.radius);
}
}
class Square extends Shape {
private side: number;
constructor(renderer: Renderer, side: number) {
super(renderer);
this.side = side;
}
draw(): void {
this.renderer.renderSquare(this.side);
}
}
// Использование
const canvasCircle = new Circle(new CanvasRenderer(), 10);
canvasCircle.draw(); // Canvas: рисуем круг радиусом 10
const svgSquare = new Square(new SVGRenderer(), 5);
svgSquare.draw(); // SVG: <rect width="5" height="5" />
Ключевая особенность: TypeScript гарантирует, что все реализации следуют контракту Renderer. Добавление нового типа фигуры или нового рендерера не ломает существующий код.
2. Visitor: обход сложных структур без изменения классов
Паттерн Visitor (Посетитель) позволяет добавлять новые операции к объектам без изменения их классов. Это идеальное решение для работы с деревьями, AST (абстрактными синтаксическими деревьями) или иерархиями компонентов.
Пример: Система экспорта узлов документа в разные форматы (JSON, XML).
// Элементы (Elements)
interface DocumentNode {
accept(visitor: Visitor): void;
}
class TextNode implements DocumentNode {
constructor(public content: string) {}
accept(visitor: Visitor): void {
visitor.visitTextNode(this);
}
}
class ImageNode implements DocumentNode {
constructor(public src: string, public alt: string) {}
accept(visitor: Visitor): void {
visitor.visitImageNode(this);
}
}
// Посетитель (Visitor)
interface Visitor {
visitTextNode(node: TextNode): void;
visitImageNode(node: ImageNode): void;
}
class JSONExporter implements Visitor {
private parts: string[] = [];
visitTextNode(node: TextNode): void {
this.parts.push(`{ "type": "text", "content": "${node.content}" }`);
}
visitImageNode(node: ImageNode): void {
this.parts.push(`{ "type": "image", "src": "${node.src}", "alt": "${node.alt}" }`);
}
getResult(): string {
return `[${this.parts.join(', ')}]`;
}
}
class XMLExporter implements Visitor {
private parts: string[] = [];
visitTextNode(node: TextNode): void {
this.parts.push(`<text>${node.content}</text>`);
}
visitImageNode(node: ImageNode): void {
this.parts.push(`<image src="${node.src}" alt="${node.alt}" />`);
}
getResult(): string {
return `<document>${this.parts.join('')}</document>`;
}
}
// Использование
const nodes: DocumentNode[] = [
new TextNode('Привет, мир!'),
new ImageNode('logo.png', 'Логотип')
];
const jsonExporter = new JSONExporter();
nodes.forEach(node => node.accept(jsonExporter));
console.log(jsonExporter.getResult());
// [{"type": "text", "content": "Привет, мир!"}, {"type": "image", "src": "logo.png", "alt": "Логотип"}]
Важно: TypeScript помогает избежать ошибок — если вы добавите новый тип узла, компилятор заставит реализовать accept и обновить все посетители.
3. State: управление конечными автоматами
Паттерн State (Состояние) позволяет объекту менять своё поведение в зависимости от внутреннего состояния. Это альтернатива громоздким switch/case конструкциям.
Пример: Заказ в интернет-магазине с состояниями: Новый, Оплачен, Отправлен, Доставлен.
interface OrderState {
pay(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
getStatus(): string;
}
class NewState implements OrderState {
pay(order: Order): void {
console.log('Заказ оплачен.');
order.setState(new PaidState());
}
ship(order: Order): void {
console.log('Нельзя отправить неоплаченный заказ.');
}
deliver(order: Order): void {
console.log('Нельзя доставить неоплаченный заказ.');
}
getStatus(): string {
return 'Новый';
}
}
class PaidState implements OrderState {
pay(order: Order): void {
console.log('Заказ уже оплачен.');
}
ship(order: Order): void {
console.log('Заказ отправлен.');
order.setState(new ShippedState());
}
deliver(order: Order): void {
console.log('Нельзя доставить неотправленный заказ.');
}
getStatus(): string {
return 'Оплачен';
}
}
class ShippedState implements OrderState {
pay(order: Order): void {
console.log('Заказ уже оплачен.');
}
ship(order: Order): void {
console.log('Заказ уже отправлен.');
}
deliver(order: Order): void {
console.log('Заказ до