Arquitectura 27 min de lectura

Patrones de Diseño en TypeScript

Publicado el 30 de enero de 2026

Patrones de Diseño en TypeScript

Los patrones de diseño son soluciones reutilizables a problemas comunes en el diseño de software. No son código específico, sino plantillas que puedes adaptar a tu situación particular. Entender patrones de diseño te ayuda a escribir código más mantenible, escalable y fácil de entender.

En este artículo, exploraremos los patrones de diseño más importantes organizados en tres categorías: creacionales, estructurales y de comportamiento. Para cada patrón, mostraré un ejemplo “sin patrón” (el problema) y “con patrón” (la solución), usando TypeScript para los ejemplos.

Los ejemplos serán pequeños y prácticos, enfocándonos en entender el concepto más que en implementaciones complejas.

Patrones Creacionales

Los patrones creacionales se enfocan en cómo se crean los objetos. Proporcionan formas de crear objetos mientras ocultan la lógica de creación.

Builder

El patrón Builder permite construir objetos complejos paso a paso. Es útil cuando un objeto tiene muchos parámetros opcionales.

❌ Sin patrón

// Constructor con muchos parámetros opcionales
class User {
    constructor(
        public name: string,
        public email: string,
        public age?: number,
        public phone?: string,
        public address?: string,
        public city?: string,
        public country?: string
    ) {}
}

// Difícil de usar y propenso a errores
const user = new User(
    "Juan",
    "juan@example.com",
    undefined, // ¿Qué parámetro es este?
    undefined,
    "Calle Principal 123",
    "Madrid"
);

✅ Con patrón Builder

class User {
    constructor(
        public name: string,
        public email: string,
        public age?: number,
        public phone?: string,
        public address?: string,
        public city?: string,
        public country?: string
    ) {}
}

class UserBuilder {
    private name!: string;
    private email!: string;
    private age?: number;
    private phone?: string;
    private address?: string;
    private city?: string;
    private country?: string;

    setName(name: string): UserBuilder {
        this.name = name;
        return this;
    }

    setEmail(email: string): UserBuilder {
        this.email = email;
        return this;
    }

    setAge(age: number): UserBuilder {
        this.age = age;
        return this;
    }

    setPhone(phone: string): UserBuilder {
        this.phone = phone;
        return this;
    }

    setAddress(address: string): UserBuilder {
        this.address = address;
        return this;
    }

    setCity(city: string): UserBuilder {
        this.city = city;
        return this;
    }

    setCountry(country: string): UserBuilder {
        this.country = country;
        return this;
    }

    build(): User {
        return new User(
            this.name,
            this.email,
            this.age,
            this.phone,
            this.address,
            this.city,
            this.country
        );
    }
}

// Uso claro y legible
const user = new UserBuilder()
    .setName("Juan")
    .setEmail("juan@example.com")
    .setAddress("Calle Principal 123")
    .setCity("Madrid")
    .build();

Factory Method

El patrón Factory Method proporciona una interfaz para crear objetos, pero permite que las subclases decidan qué clase instanciar.

❌ Sin patrón

// Lógica de creación mezclada con el código cliente
function createPayment(method: string, amount: number) {
    if (method === "credit_card") {
        return new CreditCardPayment(amount);
    } else if (method === "paypal") {
        return new PayPalPayment(amount);
    } else if (method === "crypto") {
        return new CryptoPayment(amount);
    }
    throw new Error("Invalid payment method");
}

✅ Con patrón Factory Method

interface Payment {
    process(): void;
}

class CreditCardPayment implements Payment {
    constructor(private amount: number) {}
    process() {
        console.log(`Processing credit card payment: ${this.amount}`);
    }
}

class PayPalPayment implements Payment {
    constructor(private amount: number) {}
    process() {
        console.log(`Processing PayPal payment: ${this.amount}`);
    }
}

abstract class PaymentFactory {
    abstract createPayment(amount: number): Payment;

    processPayment(amount: number): void {
        const payment = this.createPayment(amount);
        payment.process();
    }
}

class CreditCardFactory extends PaymentFactory {
    createPayment(amount: number): Payment {
        return new CreditCardPayment(amount);
    }
}

class PayPalFactory extends PaymentFactory {
    createPayment(amount: number): Payment {
        return new PayPalPayment(amount);
    }
}

// Uso
const factory = new CreditCardFactory();
factory.processPayment(100);

Abstract Factory

El patrón Abstract Factory proporciona una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas.

❌ Sin patrón

// Creación acoplada a clases concretas
class WindowsButton {
    render() {
        console.log("Windows button");
    }
}

class MacButton {
    render() {
        console.log("Mac button");
    }
}

// El código cliente debe conocer todas las clases
function createUI(os: string) {
    if (os === "windows") {
        return new WindowsButton();
    } else {
        return new MacButton();
    }
}

✅ Con patrón Abstract Factory

interface Button {
    render(): void;
}

interface Checkbox {
    render(): void;
}

class WindowsButton implements Button {
    render() {
        console.log("Windows button");
    }
}

class MacButton implements Button {
    render() {
        console.log("Mac button");
    }
}

class WindowsCheckbox implements Checkbox {
    render() {
        console.log("Windows checkbox");
    }
}

class MacCheckbox implements Checkbox {
    render() {
        console.log("Mac checkbox");
    }
}

interface UIFactory {
    createButton(): Button;
    createCheckbox(): Checkbox;
}

class WindowsFactory implements UIFactory {
    createButton(): Button {
        return new WindowsButton();
    }
    createCheckbox(): Checkbox {
        return new WindowsCheckbox();
    }
}

class MacFactory implements UIFactory {
    createButton(): Button {
        return new MacButton();
    }
    createCheckbox(): Checkbox {
        return new MacCheckbox();
    }
}

// Uso: crear familias de objetos relacionados
function createUI(factory: UIFactory) {
    const button = factory.createButton();
    const checkbox = factory.createCheckbox();
    button.render();
    checkbox.render();
}

createUI(new WindowsFactory());

Prototype

El patrón Prototype permite crear nuevos objetos copiando instancias existentes (prototipos), en lugar de crear instancias desde cero.

❌ Sin patrón

// Crear objetos desde cero cada vez
class Document {
    constructor(
        public title: string,
        public content: string,
        public author: string
    ) {}
}

// Cada vez que necesitas un documento similar, lo creas desde cero
const doc1 = new Document("Report", "Content...", "John");
const doc2 = new Document("Report", "Content...", "John"); // Duplicación

✅ Con patrón Prototype

interface Cloneable {
    clone(): Cloneable;
}

class Document implements Cloneable {
    constructor(
        public title: string,
        public content: string,
        public author: string
    ) {}

    clone(): Document {
        return new Document(this.title, this.content, this.author);
    }
}

// Crear prototipo
const reportTemplate = new Document("Report", "Template content", "System");

// Clonar cuando necesites uno nuevo
const doc1 = reportTemplate.clone();
doc1.title = "Q1 Report";

const doc2 = reportTemplate.clone();
doc2.title = "Q2 Report";

Inmutabilidad

Aunque no es un patrón tradicional del GoF, la inmutabilidad es un principio importante que previene efectos secundarios.

❌ Sin inmutabilidad

// Objetos mutables pueden causar bugs inesperados
class User {
    constructor(public name: string, public age: number) {}
}

const user = new User("Juan", 25);
user.age = 26; // Mutación directa

function updateAge(user: User, newAge: number) {
    user.age = newAge; // Efecto secundario
}

updateAge(user, 30);
console.log(user.age); // 30 - el objeto original cambió

✅ Con inmutabilidad

// Usar readonly y crear nuevos objetos en lugar de mutar
class User {
    constructor(
        public readonly name: string,
        public readonly age: number
    ) {}
}

const user = new User("Juan", 25);

// Crear nuevo objeto en lugar de mutar
function updateAge(user: User, newAge: number): User {
    return new User(user.name, newAge);
}

const updatedUser = updateAge(user, 30);
console.log(user.age); // 25 - original no cambió
console.log(updatedUser.age); // 30 - nuevo objeto

Singleton

El patrón Singleton asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a ella.

❌ Sin patrón

// Múltiples instancias pueden causar problemas
class DatabaseConnection {
    constructor() {
        console.log("Connecting to database...");
    }
}

// Cada vez que creas una instancia, se crea una nueva conexión
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection(); // Nueva conexión innecesaria

✅ Con patrón Singleton

class DatabaseConnection {
    private static instance: DatabaseConnection;

    private constructor() {
        console.log("Connecting to database...");
    }

    public static getInstance(): DatabaseConnection {
        if (!DatabaseConnection.instance) {
            DatabaseConnection.instance = new DatabaseConnection();
        }
        return DatabaseConnection.instance;
    }
}

// Siempre obtienes la misma instancia
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance(); // Misma instancia

Factory Function

Una factory function es una función que retorna un objeto. Es más simple que Factory Method y útil para crear objetos con configuración.

❌ Sin patrón

// Crear objetos directamente puede ser repetitivo
class Product {
    constructor(
        public name: string,
        public price: number,
        public category: string
    ) {}
}

const product1 = new Product("Laptop", 1000, "Electronics");
const product2 = new Product("Book", 20, "Books");
// Repetir la misma lógica de creación

✅ Con Factory Function

interface Product {
    name: string;
    price: number;
    category: string;
}

function createProduct(
    name: string,
    price: number,
    category: string
): Product {
    // Lógica de validación o configuración centralizada
    if (price < 0) {
        throw new Error("Price cannot be negative");
    }

    return {
        name,
        price,
        category,
        createdAt: new Date(), // Agregar campos automáticamente
    };
}

// Uso simple y consistente
const product1 = createProduct("Laptop", 1000, "Electronics");
const product2 = createProduct("Book", 20, "Books");

Patrones Estructurales

Los patrones estructurales se enfocan en cómo se componen las clases y objetos para formar estructuras más grandes.

Adapter

El patrón Adapter permite que interfaces incompatibles trabajen juntas.

❌ Sin patrón

// Interfaces incompatibles
class OldPaymentSystem {
    pay(amount: number) {
        console.log(`Paying ${amount} with old system`);
    }
}

class NewPaymentSystem {
    processPayment(amount: number) {
        console.log(`Processing payment of ${amount}`);
    }
}

// El código cliente debe manejar ambas interfaces
function handlePayment(system: any, amount: number) {
    if (system.pay) {
        system.pay(amount);
    } else if (system.processPayment) {
        system.processPayment(amount);
    }
}

✅ Con patrón Adapter

interface PaymentProcessor {
    process(amount: number): void;
}

class NewPaymentSystem {
    processPayment(amount: number) {
        console.log(`Processing payment of ${amount}`);
    }
}

class OldPaymentSystem {
    pay(amount: number) {
        console.log(`Paying ${amount} with old system`);
    }
}

// Adapter adapta la interfaz antigua a la nueva
class OldPaymentAdapter implements PaymentProcessor {
    constructor(private oldSystem: OldPaymentSystem) {}

    process(amount: number): void {
        this.oldSystem.pay(amount);
    }
}

// Ahora ambos sistemas usan la misma interfaz
function handlePayment(processor: PaymentProcessor, amount: number) {
    processor.process(amount);
}

const newSystem = new NewPaymentSystem();
const oldSystem = new OldPaymentAdapter(new OldPaymentSystem());

handlePayment(newSystem, 100);
handlePayment(oldSystem, 100);

Bridge

El patrón Bridge separa una abstracción de su implementación, permitiendo que ambas varíen independientemente.

❌ Sin patrón

// Combinación de abstracción e implementación
class RedCircle {
    draw() {
        console.log("Drawing red circle");
    }
}

class BlueCircle {
    draw() {
        console.log("Drawing blue circle");
    }
}

// Para cada combinación necesitas una nueva clase
// RedSquare, BlueSquare, GreenCircle, etc.

✅ Con patrón Bridge

// Implementación
interface Color {
    apply(): string;
}

class Red implements Color {
    apply() {
        return "red";
    }
}

class Blue implements Color {
    apply() {
        return "blue";
    }
}

// Abstracción
abstract class Shape {
    constructor(protected color: Color) {}

    abstract draw(): void;
}

class Circle extends Shape {
    draw() {
        console.log(`Drawing ${this.color.apply()} circle`);
    }
}

class Square extends Shape {
    draw() {
        console.log(`Drawing ${this.color.apply()} square`);
    }
}

// Ahora puedes combinar independientemente
const redCircle = new Circle(new Red());
const blueSquare = new Square(new Blue());
redCircle.draw();
blueSquare.draw();

Composite

El patrón Composite permite componer objetos en estructuras de árbol y tratar objetos individuales y composiciones de manera uniforme.

❌ Sin patrón

// Tratar objetos individuales y grupos de manera diferente
class File {
    constructor(public name: string) {}
    getSize() {
        return 100;
    }
}

class Folder {
    private files: File[] = [];
    addFile(file: File) {
        this.files.push(file);
    }
    getSize() {
        return this.files.reduce((sum, file) => sum + file.getSize(), 0);
    }
}

// El código cliente debe saber si es File o Folder
function calculateSize(item: File | Folder) {
    if (item instanceof File) {
        return item.getSize();
    } else {
        return item.getSize();
    }
}

✅ Con patrón Composite

interface FileSystemItem {
    getSize(): number;
    getName(): string;
}

class File implements FileSystemItem {
    constructor(private name: string, private size: number) {}

    getName() {
        return this.name;
    }

    getSize() {
        return this.size;
    }
}

class Folder implements FileSystemItem {
    private children: FileSystemItem[] = [];

    constructor(private name: string) {}

    getName() {
        return this.name;
    }

    add(item: FileSystemItem) {
        this.children.push(item);
    }

    getSize() {
        return this.children.reduce((sum, item) => sum + item.getSize(), 0);
    }
}

// Ahora puedes tratar File y Folder de la misma manera
function calculateSize(item: FileSystemItem) {
    return item.getSize();
}

const file1 = new File("file1.txt", 100);
const file2 = new File("file2.txt", 200);
const folder = new Folder("documents");
folder.add(file1);
folder.add(file2);

console.log(calculateSize(folder)); // 300

Decorator

El patrón Decorator permite agregar comportamiento a objetos dinámicamente sin alterar su estructura.

❌ Sin patrón

// Crear subclases para cada combinación de funcionalidades
class Coffee {
    cost() {
        return 5;
    }
}

class CoffeeWithMilk extends Coffee {
    cost() {
        return super.cost() + 2;
    }
}

class CoffeeWithSugar extends Coffee {
    cost() {
        return super.cost() + 1;
    }
}

class CoffeeWithMilkAndSugar extends Coffee {
    cost() {
        return 5 + 2 + 1;
    }
}

// Explosión de clases para cada combinación

✅ Con patrón Decorator

interface Coffee {
    cost(): number;
    description(): string;
}

class SimpleCoffee implements Coffee {
    cost() {
        return 5;
    }
    description() {
        return "Coffee";
    }
}

abstract class CoffeeDecorator implements Coffee {
    constructor(protected coffee: Coffee) {}

    abstract cost(): number;
    abstract description(): string;
}

class MilkDecorator extends CoffeeDecorator {
    cost() {
        return this.coffee.cost() + 2;
    }
    description() {
        return this.coffee.description() + ", Milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    cost() {
        return this.coffee.cost() + 1;
    }
    description() {
        return this.coffee.description() + ", Sugar";
    }
}

// Combinar decoradores dinámicamente
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

console.log(coffee.description()); // "Coffee, Milk, Sugar"
console.log(coffee.cost()); // 8

Facade

El patrón Facade proporciona una interfaz simplificada a un subsistema complejo.

❌ Sin patrón

// El cliente debe conocer y usar múltiples clases del subsistema
class CPU {
    start() {
        console.log("CPU starting");
    }
}

class Memory {
    load() {
        console.log("Memory loading");
    }
}

class HardDrive {
    read() {
        console.log("Hard drive reading");
    }
}

// El cliente debe orquestar todo manualmente
function startComputer() {
    const cpu = new CPU();
    const memory = new Memory();
    const hardDrive = new HardDrive();

    cpu.start();
    memory.load();
    hardDrive.read();
}

✅ Con patrón Facade

class CPU {
    start() {
        console.log("CPU starting");
    }
}

class Memory {
    load() {
        console.log("Memory loading");
    }
}

class HardDrive {
    read() {
        console.log("Hard drive reading");
    }
}

// Facade simplifica la interfaz
class ComputerFacade {
    private cpu: CPU;
    private memory: Memory;
    private hardDrive: HardDrive;

    constructor() {
        this.cpu = new CPU();
        this.memory = new Memory();
        this.hardDrive = new HardDrive();
    }

    start() {
        this.cpu.start();
        this.memory.load();
        this.hardDrive.read();
        console.log("Computer started");
    }
}

// El cliente solo usa la interfaz simple
const computer = new ComputerFacade();
computer.start();

Flyweight

El patrón Flyweight minimiza el uso de memoria compartiendo datos comunes entre múltiples objetos.

❌ Sin patrón

// Cada objeto almacena todos sus datos, incluso los compartidos
class Tree {
    constructor(
        public x: number,
        public y: number,
        public type: string,
        public color: string,
        public texture: string
    ) {}
}

// Crear 1000 árboles duplica datos compartidos
const trees: Tree[] = [];
for (let i = 0; i < 1000; i++) {
    trees.push(new Tree(i, i, "Oak", "green", "oak-texture.png"));
}
// Cada árbol almacena "Oak", "green", "oak-texture.png" - desperdicio de memoria

✅ Con patrón Flyweight

// Datos intrínsecos (compartidos)
class TreeType {
    constructor(
        public name: string,
        public color: string,
        public texture: string
    ) {}
}

// Flyweight Factory
class TreeTypeFactory {
    private static types: Map<string, TreeType> = new Map();

    static getTreeType(name: string, color: string, texture: string): TreeType {
        const key = `${name}-${color}-${texture}`;
        if (!this.types.has(key)) {
            this.types.set(key, new TreeType(name, color, texture));
        }
        return this.types.get(key)!;
    }
}

// Datos extrínsecos (únicos por instancia)
class Tree {
    constructor(
        public x: number,
        public y: number,
        public type: TreeType
    ) {}
}

// Ahora los datos compartidos se almacenan una sola vez
const treeType = TreeTypeFactory.getTreeType("Oak", "green", "oak-texture.png");
const trees: Tree[] = [];
for (let i = 0; i < 1000; i++) {
    trees.push(new Tree(i, i, treeType));
}
// Todos comparten el mismo TreeType - ahorro de memoria

Proxy

El patrón Proxy proporciona un sustituto o marcador de posición para otro objeto para controlar el acceso a él.

❌ Sin patrón

// Acceso directo sin control
class Image {
    constructor(private filename: string) {
        this.load(); // Carga inmediata, incluso si no se usa
    }

    private load() {
        console.log(`Loading ${this.filename}...`);
    }

    display() {
        console.log(`Displaying ${this.filename}`);
    }
}

// Carga la imagen incluso si nunca se muestra
const image = new Image("large-image.jpg");

✅ Con patrón Proxy

interface ImageInterface {
    display(): void;
}

class RealImage implements ImageInterface {
    constructor(private filename: string) {
        this.load();
    }

    private load() {
        console.log(`Loading ${this.filename}...`);
    }

    display() {
        console.log(`Displaying ${this.filename}`);
    }
}

class ImageProxy implements ImageInterface {
    private realImage: RealImage | null = null;

    constructor(private filename: string) {}

    display() {
        if (!this.realImage) {
            this.realImage = new RealImage(this.filename);
        }
        this.realImage.display();
    }
}

// La imagen solo se carga cuando se necesita
const image = new ImageProxy("large-image.jpg");
// No se carga todavía
image.display(); // Ahora se carga y muestra

Patrones de Comportamiento

Los patrones de comportamiento se enfocan en la comunicación entre objetos y la asignación de responsabilidades.

Chain of Responsibility

El patrón Chain of Responsibility permite pasar solicitudes a lo largo de una cadena de manejadores.

❌ Sin patrón

// Lógica de manejo acoplada
function processRequest(amount: number) {
    if (amount < 100) {
        console.log("Manager approves");
    } else if (amount < 1000) {
        console.log("Director approves");
    } else {
        console.log("CEO approves");
    }
}

✅ Con patrón Chain of Responsibility

interface Handler {
    setNext(handler: Handler): Handler;
    handle(amount: number): void;
}

abstract class Approver implements Handler {
    private nextHandler?: Handler;

    setNext(handler: Handler): Handler {
        this.nextHandler = handler;
        return handler;
    }

    handle(amount: number): void {
        if (this.canApprove(amount)) {
            this.approve(amount);
        } else if (this.nextHandler) {
            this.nextHandler.handle(amount);
        }
    }

    protected abstract canApprove(amount: number): boolean;
    protected abstract approve(amount: number): void;
}

class Manager extends Approver {
    protected canApprove(amount: number): boolean {
        return amount < 100;
    }
    protected approve(amount: number): void {
        console.log("Manager approves");
    }
}

class Director extends Approver {
    protected canApprove(amount: number): boolean {
        return amount < 1000;
    }
    protected approve(amount: number): void {
        console.log("Director approves");
    }
}

class CEO extends Approver {
    protected canApprove(amount: number): boolean {
        return true;
    }
    protected approve(amount: number): void {
        console.log("CEO approves");
    }
}

// Construir la cadena
const manager = new Manager();
const director = new Director();
const ceo = new CEO();

manager.setNext(director).setNext(ceo);

manager.handle(50); // Manager approves
manager.handle(500); // Director approves
manager.handle(5000); // CEO approves

Command

El patrón Command encapsula una solicitud como un objeto, permitiendo parametrizar clientes con diferentes solicitudes.

❌ Sin patrón

// Llamadas directas a métodos
class Light {
    on() {
        console.log("Light is on");
    }
    off() {
        console.log("Light is off");
    }
}

const light = new Light();
light.on();
light.off();
// No puedes deshacer, hacer cola, o registrar comandos

✅ Con patrón Command

interface Command {
    execute(): void;
    undo(): void;
}

class Light {
    private isOn = false;

    on() {
        this.isOn = true;
        console.log("Light is on");
    }

    off() {
        this.isOn = false;
        console.log("Light is off");
    }

    getState() {
        return this.isOn;
    }
}

class LightOnCommand implements Command {
    constructor(private light: Light) {}

    execute() {
        this.light.on();
    }

    undo() {
        this.light.off();
    }
}

class LightOffCommand implements Command {
    constructor(private light: Light) {}

    execute() {
        this.light.off();
    }

    undo() {
        this.light.on();
    }
}

class RemoteControl {
    private command?: Command;
    private lastCommand?: Command;

    setCommand(command: Command) {
        this.command = command;
    }

    pressButton() {
        if (this.command) {
            this.command.execute();
            this.lastCommand = this.command;
        }
    }

    pressUndo() {
        if (this.lastCommand) {
            this.lastCommand.undo();
        }
    }
}

// Uso
const light = new Light();
const lightOn = new LightOnCommand(light);
const remote = new RemoteControl();

remote.setCommand(lightOn);
remote.pressButton(); // Light is on
remote.pressUndo(); // Light is off

Iterator

El patrón Iterator proporciona una forma de acceder secuencialmente a los elementos de una colección sin exponer su representación interna.

❌ Sin patrón

// Exponer la estructura interna
class BookCollection {
    private books: string[] = [];

    add(book: string) {
        this.books.push(book);
    }

    getBooks() {
        return this.books; // Expone el array interno
    }
}

const collection = new BookCollection();
collection.add("Book 1");
collection.add("Book 2");

// El cliente depende de la implementación interna
const books = collection.getBooks();
for (let i = 0; i < books.length; i++) {
    console.log(books[i]);
}

✅ Con patrón Iterator

interface Iterator<T> {
    hasNext(): boolean;
    next(): T;
}

interface Iterable<T> {
    createIterator(): Iterator<T>;
}

class BookCollection implements Iterable<string> {
    private books: string[] = [];

    add(book: string) {
        this.books.push(book);
    }

    createIterator(): Iterator<string> {
        return new BookIterator(this.books);
    }
}

class BookIterator implements Iterator<string> {
    private index = 0;

    constructor(private books: string[]) {}

    hasNext(): boolean {
        return this.index < this.books.length;
    }

    next(): string {
        return this.books[this.index++];
    }
}

// El cliente no conoce la implementación interna
const collection = new BookCollection();
collection.add("Book 1");
collection.add("Book 2");

const iterator = collection.createIterator();
while (iterator.hasNext()) {
    console.log(iterator.next());
}

Mediator

El patrón Mediator define cómo un conjunto de objetos interactúan, promoviendo el acoplamiento débil al evitar que los objetos se refieran explícitamente entre sí.

❌ Sin patrón

// Objetos se comunican directamente entre sí
class User {
    constructor(public name: string) {}

    sendMessage(message: string, to: User) {
        console.log(`${this.name} to ${to.name}: ${message}`);
    }
}

const user1 = new User("Alice");
const user2 = new User("Bob");
const user3 = new User("Charlie");

// Cada usuario debe conocer a los demás
user1.sendMessage("Hello", user2);
user2.sendMessage("Hi", user1);
// Acoplamiento fuerte entre usuarios

✅ Con patrón Mediator

interface Mediator {
    sendMessage(message: string, from: User, to?: User): void;
}

class ChatMediator implements Mediator {
    private users: User[] = [];

    addUser(user: User) {
        this.users.push(user);
        user.setMediator(this);
    }

    sendMessage(message: string, from: User, to?: User): void {
        if (to) {
            // Mensaje privado
            console.log(`${from.name} to ${to.name}: ${message}`);
        } else {
            // Mensaje a todos
            this.users.forEach(user => {
                if (user !== from) {
                    console.log(`${from.name} to ${user.name}: ${message}`);
                }
            });
        }
    }
}

class User {
    private mediator?: Mediator;

    constructor(public name: string) {}

    setMediator(mediator: Mediator) {
        this.mediator = mediator;
    }

    send(message: string, to?: User) {
        if (this.mediator) {
            this.mediator.sendMessage(message, this, to);
        }
    }
}

// Los usuarios no se conocen entre sí, solo conocen el mediator
const mediator = new ChatMediator();
const user1 = new User("Alice");
const user2 = new User("Bob");
const user3 = new User("Charlie");

mediator.addUser(user1);
mediator.addUser(user2);
mediator.addUser(user3);

user1.send("Hello everyone"); // Broadcast
user2.send("Hi", user1); // Privado

Memento

El patrón Memento permite capturar y restaurar el estado interno de un objeto sin violar la encapsulación.

❌ Sin patrón

// Exponer estado interno para guardar/restaurar
class Editor {
    public content: string = "";

    type(text: string) {
        this.content += text;
    }
}

const editor = new Editor();
editor.type("Hello");
const savedContent = editor.content; // Exponer estado interno
editor.type(" World");

// Restaurar
editor.content = savedContent; // Violar encapsulación

✅ Con patrón Memento

class Memento {
    constructor(private state: string) {}

    getState(): string {
        return this.state;
    }
}

class Editor {
    private content: string = "";

    type(text: string) {
        this.content += text;
    }

    getContent(): string {
        return this.content;
    }

    save(): Memento {
        return new Memento(this.content);
    }

    restore(memento: Memento) {
        this.content = memento.getState();
    }
}

class History {
    private mementos: Memento[] = [];

    push(memento: Memento) {
        this.mementos.push(memento);
    }

    pop(): Memento | undefined {
        return this.mementos.pop();
    }
}

// Uso
const editor = new Editor();
const history = new History();

editor.type("Hello");
history.push(editor.save());

editor.type(" World");
console.log(editor.getContent()); // "Hello World"

editor.restore(history.pop()!);
console.log(editor.getContent()); // "Hello"

Observer

El patrón Observer define una dependencia uno-a-muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados.

❌ Sin patrón

// Acoplamiento fuerte: el sujeto debe conocer a todos los observadores
class NewsPublisher {
    private subscribers: string[] = [];

    subscribe(email: string) {
        this.subscribers.push(email);
    }

    notify(news: string) {
        // Debe conocer cómo notificar a cada tipo de suscriptor
        this.subscribers.forEach(email => {
            console.log(`Email to ${email}: ${news}`);
        });
    }
}

✅ Con patrón Observer

interface Observer {
    update(data: string): void;
}

interface Subject {
    subscribe(observer: Observer): void;
    unsubscribe(observer: Observer): void;
    notify(data: string): void;
}

class NewsPublisher implements Subject {
    private observers: Observer[] = [];

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

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

    notify(news: string) {
        this.observers.forEach(observer => observer.update(news));
    }
}

class EmailSubscriber implements Observer {
    constructor(private email: string) {}

    update(news: string) {
        console.log(`Email to ${this.email}: ${news}`);
    }
}

class SMSSubscriber implements Observer {
    constructor(private phone: string) {}

    update(news: string) {
        console.log(`SMS to ${this.phone}: ${news}`);
    }
}

// Uso
const publisher = new NewsPublisher();
const emailSub = new EmailSubscriber("user@example.com");
const smsSub = new SMSSubscriber("123456789");

publisher.subscribe(emailSub);
publisher.subscribe(smsSub);

publisher.notify("Breaking news!");

State

El patrón State permite que un objeto altere su comportamiento cuando su estado interno cambia.

❌ Sin patrón

// Lógica de estado mezclada con if/else
class Document {
    private state: string = "draft";

    publish() {
        if (this.state === "draft") {
            this.state = "published";
            console.log("Document published");
        } else if (this.state === "published") {
            console.log("Already published");
        } else if (this.state === "archived") {
            console.log("Cannot publish archived document");
        }
    }

    archive() {
        if (this.state === "draft") {
            console.log("Cannot archive draft");
        } else if (this.state === "published") {
            this.state = "archived";
            console.log("Document archived");
        } else if (this.state === "archived") {
            console.log("Already archived");
        }
    }
}

✅ Con patrón State

interface DocumentState {
    publish(): void;
    archive(): void;
}

class DraftState implements DocumentState {
    constructor(private document: Document) {}

    publish() {
        console.log("Document published");
        this.document.setState(new PublishedState(this.document));
    }

    archive() {
        console.log("Cannot archive draft");
    }
}

class PublishedState implements DocumentState {
    constructor(private document: Document) {}

    publish() {
        console.log("Already published");
    }

    archive() {
        console.log("Document archived");
        this.document.setState(new ArchivedState(this.document));
    }
}

class ArchivedState implements DocumentState {
    constructor(private document: Document) {}

    publish() {
        console.log("Cannot publish archived document");
    }

    archive() {
        console.log("Already archived");
    }
}

class Document {
    private state: DocumentState;

    constructor() {
        this.state = new DraftState(this);
    }

    setState(state: DocumentState) {
        this.state = state;
    }

    publish() {
        this.state.publish();
    }

    archive() {
        this.state.archive();
    }
}

// Uso
const doc = new Document();
doc.publish(); // Document published
doc.archive(); // Document archived

Strategy

El patrón Strategy define una familia de algoritmos, los encapsula y los hace intercambiables.

❌ Sin patrón

// Lógica de algoritmos mezclada con if/else
class PaymentProcessor {
    process(amount: number, method: string) {
        if (method === "credit_card") {
            console.log(`Processing ${amount} with credit card (fee: 2%)`);
            return amount * 1.02;
        } else if (method === "paypal") {
            console.log(`Processing ${amount} with PayPal (fee: 3%)`);
            return amount * 1.03;
        } else if (method === "crypto") {
            console.log(`Processing ${amount} with crypto (fee: 1%)`);
            return amount * 1.01;
        }
    }
}

✅ Con patrón Strategy

interface PaymentStrategy {
    process(amount: number): number;
}

class CreditCardStrategy implements PaymentStrategy {
    process(amount: number): number {
        console.log(`Processing ${amount} with credit card (fee: 2%)`);
        return amount * 1.02;
    }
}

class PayPalStrategy implements PaymentStrategy {
    process(amount: number): number {
        console.log(`Processing ${amount} with PayPal (fee: 3%)`);
        return amount * 1.03;
    }
}

class CryptoStrategy implements PaymentStrategy {
    process(amount: number): number {
        console.log(`Processing ${amount} with crypto (fee: 1%)`);
        return amount * 1.01;
    }
}

class PaymentProcessor {
    private strategy?: PaymentStrategy;

    setStrategy(strategy: PaymentStrategy) {
        this.strategy = strategy;
    }

    process(amount: number): number {
        if (!this.strategy) {
            throw new Error("No payment strategy set");
        }
        return this.strategy.process(amount);
    }
}

// Uso
const processor = new PaymentProcessor();
processor.setStrategy(new CreditCardStrategy());
processor.process(100); // Processing 100 with credit card (fee: 2%)

processor.setStrategy(new CryptoStrategy());
processor.process(100); // Processing 100 with crypto (fee: 1%)

Template Method

El patrón Template Method define el esqueleto de un algoritmo en una clase base, permitiendo que las subclases sobrescriban pasos específicos.

❌ Sin patrón

// Duplicación de código en algoritmos similares
class CoffeeMaker {
    makeCoffee() {
        console.log("Boiling water");
        console.log("Brewing coffee");
        console.log("Pouring into cup");
        console.log("Adding sugar and milk");
    }
}

class TeaMaker {
    makeTea() {
        console.log("Boiling water");
        console.log("Steeping tea");
        console.log("Pouring into cup");
        console.log("Adding lemon");
    }
}
// Mucho código duplicado

✅ Con patrón Template Method

abstract class BeverageMaker {
    // Template method: define el algoritmo
    make(): void {
        this.boilWater();
        this.brew();
        this.pourInCup();
        this.addCondiments();
    }

    // Pasos comunes
    private boilWater() {
        console.log("Boiling water");
    }

    private pourInCup() {
        console.log("Pouring into cup");
    }

    // Pasos que varían - deben ser implementados por subclases
    protected abstract brew(): void;
    protected abstract addCondiments(): void;
}

class CoffeeMaker extends BeverageMaker {
    protected brew() {
        console.log("Brewing coffee");
    }

    protected addCondiments() {
        console.log("Adding sugar and milk");
    }
}

class TeaMaker extends BeverageMaker {
    protected brew() {
        console.log("Steeping tea");
    }

    protected addCondiments() {
        console.log("Adding lemon");
    }
}

// Uso
const coffee = new CoffeeMaker();
coffee.make();

const tea = new TeaMaker();
tea.make();

Visitor

El patrón Visitor permite definir nuevas operaciones sobre objetos sin cambiar sus clases.

❌ Sin patrón

// Agregar nuevas operaciones requiere modificar las clases
interface Animal {
    name: string;
}

class Dog implements Animal {
    name = "Dog";
    // Si quiero agregar una operación, debo modificar esta clase
}

class Cat implements Animal {
    name = "Cat";
    // Y esta también
}

function makeSound(animal: Animal) {
    if (animal instanceof Dog) {
        console.log("Woof");
    } else if (animal instanceof Cat) {
        console.log("Meow");
    }
    // Agregar nuevo animal requiere modificar esta función
}

✅ Con patrón Visitor

interface Animal {
    accept(visitor: AnimalVisitor): void;
}

interface AnimalVisitor {
    visitDog(dog: Dog): void;
    visitCat(cat: Cat): void;
}

class Dog implements Animal {
    name = "Dog";

    accept(visitor: AnimalVisitor) {
        visitor.visitDog(this);
    }
}

class Cat implements Animal {
    name = "Cat";

    accept(visitor: AnimalVisitor) {
        visitor.visitCat(this);
    }
}

class SoundVisitor implements AnimalVisitor {
    visitDog(dog: Dog) {
        console.log("Woof");
    }

    visitCat(cat: Cat) {
        console.log("Meow");
    }
}

class FeedVisitor implements AnimalVisitor {
    visitDog(dog: Dog) {
        console.log("Feeding dog with bones");
    }

    visitCat(cat: Cat) {
        console.log("Feeding cat with fish");
    }
}

// Agregar nuevas operaciones sin modificar las clases Animal
const dog = new Dog();
const cat = new Cat();

const soundVisitor = new SoundVisitor();
dog.accept(soundVisitor); // Woof
cat.accept(soundVisitor); // Meow

const feedVisitor = new FeedVisitor();
dog.accept(feedVisitor); // Feeding dog with bones
cat.accept(feedVisitor); // Feeding cat with fish

Cuándo Usar Cada Patrón

Patrones Creacionales

  • Builder: Cuando un objeto tiene muchos parámetros opcionales
  • Factory Method: Cuando necesitas desacoplar la creación de objetos de su uso
  • Abstract Factory: Cuando necesitas crear familias de objetos relacionados
  • Prototype: Cuando crear objetos es costoso y puedes clonar instancias existentes
  • Singleton: Cuando necesitas exactamente una instancia de una clase
  • Factory Function: Cuando necesitas lógica de creación simple y reutilizable

Patrones Estructurales

  • Adapter: Cuando necesitas hacer que interfaces incompatibles trabajen juntas
  • Bridge: Cuando quieres separar abstracción de implementación
  • Composite: Cuando necesitas tratar objetos individuales y grupos uniformemente
  • Decorator: Cuando necesitas agregar comportamiento dinámicamente
  • Facade: Cuando quieres simplificar una interfaz compleja
  • Flyweight: Cuando necesitas optimizar memoria compartiendo datos comunes
  • Proxy: Cuando necesitas controlar el acceso a un objeto

Patrones de Comportamiento

  • Chain of Responsibility: Cuando múltiples objetos pueden manejar una solicitud
  • Command: Cuando necesitas parametrizar objetos con operaciones
  • Iterator: Cuando necesitas acceder a elementos de una colección sin exponer su estructura
  • Mediator: Cuando quieres reducir el acoplamiento entre objetos que se comunican
  • Memento: Cuando necesitas guardar y restaurar el estado de un objeto
  • Observer: Cuando un cambio en un objeto requiere actualizar otros objetos
  • State: Cuando el comportamiento de un objeto depende de su estado
  • Strategy: Cuando tienes múltiples algoritmos y quieres hacerlos intercambiables
  • Template Method: Cuando tienes algoritmos similares con pasos que varían
  • Visitor: Cuando necesitas agregar operaciones sin modificar las clases

Mi perspectiva personal

Los patrones de diseño no son reglas que debes seguir ciegamente. Son herramientas que debes tener en tu caja de herramientas y usar cuando tienen sentido.

He visto proyectos que usan patrones innecesariamente, agregando complejidad sin beneficio. He visto proyectos que se beneficiarían enormemente de usar patrones pero no los usan, resultando en código acoplado y difícil de mantener.

La clave es entender cuándo un patrón resuelve un problema real que tienes, no aplicar patrones porque “deberías usarlos”. Si tienes un objeto con muchos parámetros opcionales, Builder tiene sentido. Si necesitas desacoplar la creación de objetos, Factory Method tiene sentido. Si necesitas agregar comportamiento dinámicamente, Decorator tiene sentido.

TypeScript hace que muchos patrones sean más seguros y expresivos. El tipado estático ayuda a detectar errores temprano y hace que las interfaces de los patrones sean más claras.

No intentes usar todos los patrones en un solo proyecto. Usa los que resuelven problemas específicos que tienes. Y recuerda: los patrones son medios para un fin (código mantenible y escalable), no un fin en sí mismos.

Al final del día, los patrones de diseño son sobre escribir código que sea fácil de entender, modificar y extender. Si un patrón hace tu código más difícil de entender, no lo uses. Si hace tu código más claro y mantenible, úsalo.