Patrones de Diseño: El Lenguaje Universal que Todo Developer Debería Hablar

# programming# webdev# java# typescript
Patrones de Diseño: El Lenguaje Universal que Todo Developer Debería Hablarjesus manrique

Qué son los patrones de diseño, por qué importan más que nunca, y ejemplos reales en Java y TypeScript: Singleton, Strategy y Observer explicados sin academicismo.

Patrones de Diseño — Header


La primera vez que leí sobre patrones de diseño fue en un PDF escaneado del libro del Gang of Four. Duré 15 páginas antes de cerrarlo. Demasiada teoría. Demasiado UML. Demasiado "¿esto cuándo lo voy a usar en mi vida?"

Avance rápido 8 años. Hoy reviso ese mismo libro y lo leo diferente. Ya viví los dolores que esos patrones resuelven. Ya escribí el código acoplado que el Strategy desacopla, la clase de 3,000 líneas que el Observer parte en pedazos, el switch statement infinito que el Factory Method elimina.

Los patrones de diseño no se aprenden leyendo — se aprenden sufriendo. Pero si este artículo te ahorra aunque sea un dolor de cabeza, ya valió la pena escribirlo.


¿Qué es un patrón de diseño?

La definición formal dice que un patrón de diseño es una solución comprobada a un problema recurrente en el diseño de software. Suena corporativo. En español real: es un truco elegante que alguien más ya descubrió, ya probó, y ya documentó para que tú no tengas que reinventarlo a las 2 AM.

No son librerías. No son código que copias y pegas. Son más bien como planos: entiendes la idea, la adaptas a tu contexto, y la implementas en tu lenguaje.

Piénsalo así: los arquitectos no diseñan cada edificio desde cero. Tienen soluciones probadas para cosas como "cómo iluminar un pasillo interior" o "cómo drenar un techo plano." Los patterns son eso mismo, pero para software.


¿Por qué deberían importarte?

1. Te dan vocabulario

Imagina esta conversación sin patrones:

—"Oye, necesito que para la conexión a la base de datos crees una clase que no permita que nadie haga new desde fuera, que tenga un método estático que devuelva siempre la misma instancia verificando si ya existe, y que almacene esa instancia en un campo privado para que todo el sistema use exactamente el mismo objeto."

Con patrones:

—"Usa un Singleton para la conexión a la BD."

Tres palabras. El que habla no tuvo que explicar la estructura. El que escucha no tuvo que pedir un diagrama. Ambos visualizan inmediatamente la misma solución.

Los patrones son el equivalente en software de lo que "acorde de Do mayor" es para los músicos. No tienes que decir "pon el dedo índice en el primer traste de la segunda cuerda, el dedo medio en..." — dices "Do mayor" y todos saben qué tocar.

Ese vocabulario compartido es lo que permite que un equipo de 5 developers discuta una arquitectura compleja en 20 minutos en vez de 3 horas. Y es lo que hace que cuando lees "esta clase usa Observer" en un README, entiendas el 60% de su diseño antes de abrir el archivo.

2. Son soluciones déjà vu

El 90% de los problemas de diseño que enfrentas ya los vivió alguien en 1994. Esos problemas están tan estudiados que las soluciones tienen nombre, ejemplos, trade-offs documentados y libros enteros dedicados a cada uno.

3. Te vuelven mejor revisor de código

Conocer patrones no solo te ayuda a escribir mejor — te ayuda a LEER mejor. Cuando abres un codebase ajeno y reconoces un Observer en el event system, un Factory en el DI container, o un Decorator en los middleware, entiendes la arquitectura a la primera. Dejas de leer línea por línea y empiezas a ver la forma del código.

4. Son el puente entre junior y senior

Hay muchas diferencias entre un junior y un senior, pero una de las más notorias es esta: un junior resuelve un problema pensando en el presente inmediato. Un senior reconoce la forma del problema, recuerda qué patrón lo resuelve, y lo aplica sabiendo cómo escalará en 6 meses. Los patrones son atajos de experiencia.


Los 3 grandes grupos

Los 23 patrones clásicos del Gang of Four se agrupan en tres familias según su propósito:

Patrones Creacionales (5)

Factory Method, Abstract Factory, Builder, Prototype, Singleton

Responden a una pregunta: ¿cómo creo objetos sin acoplarme a clases concretas? Encapsulan la lógica de instanciación para que tu código dependa de interfaces, no de new ClaseConcreta().

Patrones Estructurales (7)

Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

Responden a: ¿cómo ensamblo objetos y clases manteniendo la flexibilidad? Te ayudan a componer piezas sin que todo explote cuando agregas una nueva.

Patrones de Comportamiento (11)

Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor

Responden a: ¿cómo organizo la comunicación y responsabilidades entre objetos? Son los que más vas a usar en el día a día.


Ejemplos reales en Java y TypeScript

No voy a explicar los 23. Eso sería un libro. Voy a mostrarte los 3 que más aparecen en producción, con código que puedes correr.

1. Singleton — Una instancia, un punto de acceso global

Cuándo usarlo: Cuando necesitas exactamente una instancia de una clase. Conexiones a base de datos, configuraciones globales, sistemas de logging, caches.

Java:

public class DatabaseConnection {
    // volatile para visibilidad entre threads
    private static volatile DatabaseConnection instance;
    private final String connectionString;

    // Constructor privado — nadie puede hacer new
    private DatabaseConnection() {
        this.connectionString = "jdbc:postgresql://localhost:5432/mydb";
        System.out.println("🔌 Conectando a " + connectionString);
    }

    // Doble chequeo para thread-safety sin overhead de synchronized cada vez
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnection.class) {
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }

    public void query(String sql) {
        System.out.println("⚡ Ejecutando: " + sql);
    }
}

// Uso
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();
System.out.println(db1 == db2); // true — misma instancia
Enter fullscreen mode Exit fullscreen mode

TypeScript:

class ConfigManager {
    private static instance: ConfigManager;
    private config: Record<string, string> = {};

    private constructor() {
        this.config = {
            apiUrl: "https://api.guayoyo.tech",
            maxRetries: "3",
        };
        console.log("⚙️  Configuración cargada");
    }

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

    get(key: string): string | undefined {
        return this.config[key];
    }

    set(key: string, value: string): void {
        this.config[key] = value;
    }
}

// Uso
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2);      // true
console.log(config1.get("apiUrl"));    // "https://api.guayoyo.tech"
Enter fullscreen mode Exit fullscreen mode

⚠️ Cuándo NO usarlo: El Singleton es el patrón más abusado. No lo uses para todo. Si tu clase tiene estado mutable que cambia con el tiempo, un Singleton puede ser un dolor de cabeza en testing y en aplicaciones multi-hilo. Úsalo cuando realmente necesites UNA y solo UNA instancia.


2. Strategy — Algoritmos intercambiables

Cuándo usarlo: Cuando tienes múltiples formas de hacer algo y quieres poder cambiarlas en tiempo de ejecución. Procesadores de pago, estrategias de descuento, distintos algoritmos de compresión, filtros de búsqueda.

Escenario real: procesador de pagos que acepta múltiples métodos.

Java:

// Interfaz común
interface PaymentStrategy {
    void pay(double amount);
}

// Estrategias concretas
class CardPayment implements PaymentStrategy {
    private final String cardNumber;

    public CardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(double amount) {
        System.out.printf("💳 Pagando $%.2f con tarjeta %s%n",
            amount, maskCard(cardNumber));
    }

    private String maskCard(String number) {
        return "****-****-****-" + number.substring(number.length() - 4);
    }
}

class CryptoPayment implements PaymentStrategy {
    private final String wallet;

    public CryptoPayment(String wallet) {
        this.wallet = wallet;
    }

    @Override
    public void pay(double amount) {
        System.out.printf("₿ Pagando $%.2f con wallet %s%n",
            amount, wallet.substring(0, 8) + "...");
    }
}

class BankTransferPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.printf("🏦 Transfiriendo $%.2f vía transferencia bancaria%n",
            amount);
    }
}

// Contexto — usa la estrategia pero no sabe los detalles
class PaymentProcessor {
    private PaymentStrategy strategy;

    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void process(double amount) {
        if (strategy == null) {
            throw new IllegalStateException("Selecciona un método de pago");
        }
        strategy.pay(amount);
    }
}

// Uso
var processor = new PaymentProcessor();

processor.setStrategy(new CardPayment("4532123456789012"));
processor.process(149.99);
// 💳 Pagando $149.99 con tarjeta ****-****-****-9012

processor.setStrategy(new CryptoPayment("0x71C7656EC7ab88b098defB751B7401B5f6d8976F"));
processor.process(149.99);
// ₿ Pagando $149.99 con wallet 0x71C765...
Enter fullscreen mode Exit fullscreen mode

TypeScript:

// Interfaz (en TS podemos usar type o interface)
interface PaymentStrategy {
    pay(amount: number): void;
    methodName: string;
}

// Estrategias concretas
class CardPayment implements PaymentStrategy {
    methodName = "Tarjeta";

    constructor(private cardNumber: string) {}

    pay(amount: number): void {
        console.log(
            `💳 Pagando $${amount.toFixed(2)} con tarjeta ****-${this.cardNumber.slice(-4)}`
        );
    }
}

class PagoMovilPayment implements PaymentStrategy {
    methodName = "Pago Móvil";

    constructor(private phone: string, private bank: string) {}

    pay(amount: number): void {
        console.log(
            `📱 Pagando $${amount.toFixed(2)} vía Pago Móvil (${this.bank}, ${this.phone})`
        );
    }
}

class CryptoPayment implements PaymentStrategy {
    methodName = "Cripto";

    constructor(private wallet: string) {}

    pay(amount: number): void {
        console.log(
            `₿ Pagando $${amount.toFixed(2)} con wallet ${this.wallet.slice(0, 8)}...`
        );
    }
}

// Contexto
class PaymentProcessor {
    private strategy: PaymentStrategy | null = null;

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

    process(amount: number): void {
        if (!this.strategy) {
            throw new Error("Selecciona un método de pago");
        }
        console.log(`\n🧾 Procesando factura por: $${amount.toFixed(2)}`);
        this.strategy.pay(amount);
    }
}

// Uso
const processor = new PaymentProcessor();

processor.setStrategy(new CardPayment("4532123456789012"));
processor.process(149.99);

processor.setStrategy(
    new PagoMovilPayment("0414-0108660", "Banco de Venezuela")
);
processor.process(89.50);

processor.setStrategy(
    new CryptoPayment("0x71C7656EC7ab88b098defB751B7401B5f6d8976F")
);
processor.process(200.00);
Enter fullscreen mode Exit fullscreen mode

La belleza del Strategy: agregar un nuevo método de pago (PayPal, Zelle, Efectivo) es crear UNA clase nueva. No tocas el PaymentProcessor. No modificas código que ya funciona. Eso es el Open/Closed Principle en acción — abierto para extensión, cerrado para modificación.


3. Observer — Reaccionar a eventos sin acoplamiento

Cuándo usarlo: Cuando un cambio de estado en un objeto debe notificar a muchos otros automáticamente. Notificaciones, event systems, sincronización de UI, WebSocket handlers, arquitecturas pub/sub.

Escenario real: un sistema de monitoreo de servidores.

Java:

import java.util.*;

// Interfaz del suscriptor
interface ServerObserver {
    void update(String serverId, String event);
}

// Notificador (Publisher)
class ServerMonitor {
    private final Map<String, List<ServerObserver>> subscriptions = new HashMap<>();

    public void subscribe(String serverId, ServerObserver observer) {
        subscriptions
            .computeIfAbsent(serverId, k -> new ArrayList<>())
            .add(observer);
    }

    public void unsubscribe(String serverId, ServerObserver observer) {
        List<ServerObserver> obs = subscriptions.get(serverId);
        if (obs != null) obs.remove(observer);
    }

    public void alert(String serverId, String event) {
        System.out.printf("🔔 [%s] %s%n", serverId, event);
        List<ServerObserver> obs = subscriptions.get(serverId);
        if (obs != null) {
            for (ServerObserver observer : obs) {
                observer.update(serverId, event);
            }
        }
    }
}

// Suscriptores concretos
class SlackNotifier implements ServerObserver {
    @Override
    public void update(String serverId, String event) {
        System.out.printf("  📨 Slack → #oncall: Servidor %s — %s%n", serverId, event);
    }
}

class DatabaseLogger implements ServerObserver {
    @Override
    public void update(String serverId, String event) {
        System.out.printf("  🗄️  DB → INSERT INTO alerts (server, event, timestamp) "
            + "VALUES ('%s', '%s', NOW())%n", serverId, event);
    }
}

class AutoScaler implements ServerObserver {
    @Override
    public void update(String serverId, String event) {
        if (event.contains("CPU > 90%")) {
            System.out.printf("  ⚡ AutoScaler → Escalando servidor %s%n", serverId);
        }
    }
}

// Uso
var monitor = new ServerMonitor();

monitor.subscribe("prod-web-01", new SlackNotifier());
monitor.subscribe("prod-web-01", new DatabaseLogger());
monitor.subscribe("prod-web-01", new AutoScaler());

monitor.alert("prod-web-01", "CPU > 90% por 2 minutos");
// 🔔 [prod-web-01] CPU > 90% por 2 minutos
//   📨 Slack → #oncall: Servidor prod-web-01 — CPU > 90% por 2 minutos
//   🗄️  DB → INSERT INTO alerts (...)
//   ⚡ AutoScaler → Escalando servidor prod-web-01
Enter fullscreen mode Exit fullscreen mode

TypeScript:

// Interfaces
interface ServerObserver {
    update(serverId: string, event: string): void;
}

// Notificador
class ServerMonitor {
    private subscriptions = new Map<string, ServerObserver[]>();

    subscribe(serverId: string, observer: ServerObserver): void {
        const existing = this.subscriptions.get(serverId) ?? [];
        existing.push(observer);
        this.subscriptions.set(serverId, existing);
    }

    unsubscribe(serverId: string, observer: ServerObserver): void {
        const observers = this.subscriptions.get(serverId);
        if (observers) {
            this.subscriptions.set(
                serverId,
                observers.filter((obs) => obs !== observer)
            );
        }
    }

    alert(serverId: string, event: string): void {
        console.log(`🔔 [${serverId}] ${event}`);
        const observers = this.subscriptions.get(serverId);
        if (observers) {
            observers.forEach((obs) => obs.update(serverId, event));
        }
    }
}

// Suscriptores
class SlackNotifier implements ServerObserver {
    update(serverId: string, event: string): void {
        console.log(
            `  📨 Slack → #oncall: Servidor ${serverId}${event}`
        );
    }
}

class PagerDutyNotifier implements ServerObserver {
    update(serverId: string, event: string): void {
        if (event.includes("CRÍTICO")) {
            console.log(
                `  🚨 PagerDuty → ESCALANDO: ${serverId} requiere atención inmediata`
            );
        }
    }
}

class MetricsLogger implements ServerObserver {
    update(serverId: string, event: string): void {
        const timestamp = new Date().toISOString();
        console.log(
            `  📊 Metrics → { server: "${serverId}", event: "${event}", ts: "${timestamp}" }`
        );
    }
}

// Uso en un sistema de monitoreo real
const monitor = new ServerMonitor();

monitor.subscribe("prod-api-03", new SlackNotifier());
monitor.subscribe("prod-api-03", new PagerDutyNotifier());
monitor.subscribe("prod-api-03", new MetricsLogger());

monitor.alert("prod-api-03", "CRÍTICO: Disco al 98%");
Enter fullscreen mode Exit fullscreen mode

El Observer es ubicuo en desarrollo moderno: Redux es Observer. Los WebSocket handlers son Observer. Los event listeners del DOM son Observer. Entender el patrón te hace entender media docena de librerías y frameworks automáticamente.


Cómo empezar sin abrumarte

No intentes memorizar los 23 patrones. Es inútil. Mejor:

  1. Aprende los 5 esenciales primero: Singleton, Factory Method, Strategy, Observer y Decorator. Cubren el 80% de los casos reales.

  2. Identifícalos en tu stack: Spring Boot está lleno de Template Method y Proxy. Angular usa Observer a través de RxJS. React depende de Observer con su Virtual DOM. Node.js usa Strategy en los middlewares de Express. Encuéntralos en lo que YA usas.

  3. Practica la refactorización inversa: Toma código viejo que huele mal — una clase de 500 líneas, un if/else de 20 ramas — y pregúntate: ¿qué patrón podría domesticar esto? Luego refactoriza aplicándolo.

  4. Lee Refactoring.Guru: Es el mejor recurso gratuito que existe. Explicaciones visuales, pseudocódigo, ejemplos en 10 lenguajes y cero academicismo innecesario. Si solo tomas una recomendación de este artículo, que sea esa: refactoring.guru/es

  5. No conviertas todo en un patrón: Este es el error del recién convertido. Empiezas a ver Singletons y hay Singletons hasta en la sopa. El patrón debe simplificar, no complicar. Si aplicar un patrón hace tu código más difícil de leer, no lo apliques. Simple > Elegante.


Conclusión

Los patrones de diseño no te hacen mejor programador por saber sus nombres. Te hacen mejor programador porque te enseñan a pensar en estructuras, en responsabilidades, en consecuencias a largo plazo.

Son el equivalente en código de saber jugadas de ajedrez en lugar de mover piezas viendo qué pasa. Puedes jugar sin conocerlas. Pero conocidas, el juego se vuelve diferente.

Y lo mejor: una vez que empiezas a reconocer patrones, dejas de ver código. Empiezas a ver formas, intenciones, estructura. El código de otros — y el tuyo — se lee con otros ojos.


¿Ya usas patrones de diseño en tu día a día sin darte cuenta? ¿Cuál fue el primero que aprendiste? Cuéntamelo. El mío fue Singleton — y lo usé mal por dos años hasta que entendí cuándo NO usarlo.