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
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"
⚠️ 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...
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);
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
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%");
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:
Aprende los 5 esenciales primero: Singleton, Factory Method, Strategy, Observer y Decorator. Cubren el 80% de los casos reales.
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.
Practica la refactorización inversa: Toma código viejo que huele mal — una clase de 500 líneas, un
if/elsede 20 ramas — y pregúntate: ¿qué patrón podría domesticar esto? Luego refactoriza aplicándolo.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
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.

Top comments (0)