patrón · intermedio · 17 min de lectura
Patrones de Diseño
Los patrones de diseño son soluciones probadas y con nombre para problemas recurrentes en el diseño de software orientado a objetos, organizados en tres familias: creacionales, estructurales y de comportamiento.
No requiere conocimientos previos.
Alcance en breve
Cubre
- design patterns as proven, named solutions to recurring design problems in object-oriented systems
- the three GoF categories: creational, structural, and behavioral
- key patterns from each category with intent, structure, and TypeScript examples
- the difference between using a pattern and over-engineering with patterns
- how patterns relate to SOLID principles
No cubre
- exhaustive catalog of all 23 GoF patterns
- enterprise integration patterns, concurrency patterns, and architectural patterns
- pattern implementation details across multiple languages beyond TypeScript
Supone
- The reader understands object-oriented programming: classes, interfaces, inheritance, and composition.
- The reader has encountered design problems where the «obvious» solution became hard to maintain as requirements grew.
Resumen
Un patrón de diseño es una solución reutilizable para un problema común en el diseño de software. No es código que copiás y pegás: es una plantilla conceptual —una estructura de clases, una forma de relacionar objetos, un contrato de colaboración— que adaptás a tu contexto específico. El catálogo canónico fue publicado en 1994 por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides —los "Gang of Four" o GoF— en el libro Design Patterns: Elements of Reusable Object-Oriented Software.
Importan porque le ponen nombre a soluciones que los buenos diseñadores redescubrían una y otra vez. Decir «acá usamos un Observer» comunica en una palabra la estructura completa de la solución: quién observa a quién, cómo se notifican los cambios y por qué está desacoplado. Esa economía de vocabulario acelera la comunicación técnica y la revisión de código.
Los 23 patrones originales del GoF se agrupan en tres categorías según el tipo de problema que resuelven: creacionales (cómo crear objetos), estructurales (cómo componer clases y objetos) y de comportamiento (cómo los objetos colaboran y se reparten responsabilidades). Desde entonces surgieron catálogos adicionales —patrones de integración empresarial, de concurrencia, de arquitectura—, pero los 23 del GoF siguen siendo la base.
Alcance y supuestos
Este paquete cubre los patrones de diseño como concepto: qué son, las tres categorías del GoF con ejemplos representativos de cada una, cómo se relacionan con los principios SOLID, y el riesgo de sobre-ingeniería cuando se aplican sin necesidad. No es un catálogo exhaustivo de los 23 patrones, pero da suficiente contexto para que puedas explorar cualquiera de ellos por tu cuenta.
No cubre patrones de arquitectura (MVC, microservicios, event sourcing), patrones de integración empresarial, ni patrones de concurrencia. Tampoco cubre implementaciones específicas en frameworks (aunque muchos usan patrones internamente: Angular usa Observer y Dependency Injection, React usa Composite y Visitor).
Asume que entendés POO y los principios SOLID. Si no, los nodos correspondientes cubren esos fundamentos.
Modelo mental
Pensá en los planos de un arquitecto. Un arquitecto no diseña cada casa desde cero: tiene un repertorio de soluciones para problemas comunes. «Acá necesito un patio interno para iluminar esta habitación sin ventanas externas.» «Esta zona necesita un entrepiso para ganar metros sin ampliar la huella.» Esas soluciones tienen nombre, estructura documentada y variantes según el clima y el terreno.
Los patrones de diseño son los planos del arquitecto de software. Strategy es «necesito variar un algoritmo sin cambiar quien lo usa». Adapter es «necesito conectar dos interfaces que no fueron diseñadas para trabajar juntas». Decorator es «necesito agregar comportamiento a un objeto sin modificar su clase ni afectar a otras instancias».
Cada patrón documenta cuatro cosas: el nombre, el problema que resuelve (intent), la solución (estructura de clases y colaboración), y las consecuencias (pros y contras de aplicarlo).
Uso práctico
Los patrones son herramientas, no objetivos. No diseñes pensando «¿qué patrón puedo meter acá?». Diseñá pensando en el problema, y cuando la solución se parezca a un patrón que conocés, usalo explícitamente y documentalo como tal:
- ✅ Detectás que un
switchcrece cada vez que agregás un tipo nuevo → Strategy o Factory Method. - ✅ Necesitás que varios componentes reaccionen a cambios en otro sin acoplarlos → Observer.
- ✅ Necesitás usar una librería con una interfaz incompatible con tu código → Adapter.
- ❌ Tu sistema tiene 3 clases y 50 líneas. Meter un Abstract Factory para «estar preparado» es sobre-ingeniería.
- ❌ Aplicar un patrón solo porque lo leíste y te gustó, sin que el problema lo justifique.
Las tres categorías
Creacionales
Abstraen el proceso de creación de objetos. Separan el código que usa objetos del código que los construye.
Singleton: garantiza que una clase tenga una sola instancia y provee un punto de acceso global.
class AppConfig {
private static instance: AppConfig;
private constructor(readonly dbUrl: string) {}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig(process.env.DATABASE_URL!);
}
return AppConfig.instance;
}
}
Usalo para recursos que genuinamente deben ser únicos: configuración, connection pools, loggers. No lo uses para evadir pasar dependencias por parámetro —eso viola DIP y dificulta el testing.
Factory Method: define una interfaz para crear un objeto, pero deja que las subclases decidan qué clase instanciar.
interface Document {
open(): void;
}
abstract class Application {
abstract createDocument(): Document;
newDocument() { const doc = this.createDocument(); doc.open(); }
}
class TextApplication extends Application {
createDocument(): Document { return new TextDocument(); }
}
Builder: separa la construcción de un objeto complejo de su representación, permitiendo crear distintas representaciones con el mismo proceso.
class RequestBuilder {
private method = "GET";
private headers: Record<string, string> = {};
private body: string | null = null;
setMethod(m: string) { this.method = m; return this; }
setHeader(k: string, v: string) { this.headers[k] = v; return this; }
setBody(b: string) { this.body = b; return this; }
build() { return { method: this.method, headers: this.headers, body: this.body }; }
}
const request = new RequestBuilder()
.setMethod("POST")
.setHeader("Content-Type", "application/json")
.setBody(JSON.stringify({ name: "Ana" }))
.build();
Estructurales
Describen cómo componer clases y objetos para formar estructuras más grandes, manteniendo la flexibilidad.
Adapter: convierte la interfaz de una clase en otra que el cliente espera. Permite que clases con interfaces incompatibles trabajen juntas.
// Interfaz que tu aplicación espera
interface PaymentProcessor {
pay(amount: number): void;
}
// Librería externa con interfaz distinta
class StripeAPI {
charge(amountInCents: number, currency: string) { /* ... */ }
}
class StripeAdapter implements PaymentProcessor {
constructor(private stripe: StripeAPI) {}
pay(amount: number) { this.stripe.charge(amount * 100, "usd"); }
}
Decorator: agrega responsabilidades a un objeto dinámicamente, sin modificar su clase.
interface Coffee { cost(): number; description(): string; }
class SimpleCoffee implements Coffee {
cost() { return 2; }
description() { return "Coffee"; }
}
class MilkDecorator implements Coffee {
constructor(private coffee: Coffee) {}
cost() { return this.coffee.cost() + 0.5; }
description() { return this.coffee.description() + ", milk"; }
}
const order = new MilkDecorator(new SimpleCoffee());
order.cost(); // 2.5
order.description(); // "Coffee, milk"
Composite: compone objetos en estructuras de árbol para representar jerarquías parte-todo. Permite tratar objetos individuales y composiciones de manera uniforme.
interface FileSystemNode { size(): number; }
class File implements FileSystemNode {
constructor(private bytes: number) {}
size() { return this.bytes; }
}
class Folder implements FileSystemNode {
private children: FileSystemNode[] = [];
add(node: FileSystemNode) { this.children.push(node); }
size() { return this.children.reduce((sum, c) => sum + c.size(), 0); }
}
De comportamiento
Describen cómo los objetos colaboran, se comunican y se reparten responsabilidades.
Strategy: define una familia de algoritmos, los encapsula y los hace intercambiables. Permite variar el algoritmo independientemente del cliente que lo usa.
interface SortStrategy { sort(data: number[]): number[]; }
class QuickSort implements SortStrategy {
sort(data: number[]) { /* ... */ return data; }
}
class MergeSort implements SortStrategy {
sort(data: number[]) { /* ... */ return data; }
}
class DataProcessor {
constructor(private strategy: SortStrategy) {}
process(data: number[]) { return this.strategy.sort(data); }
}
Observer: define una dependencia uno-a-muchos. Cuando un objeto cambia de estado, todos sus dependientes son notificados automáticamente.
interface Observer { update(data: unknown): void; }
class Subject {
private observers: Observer[] = [];
attach(o: Observer) { this.observers.push(o); }
detach(o: Observer) { this.observers = this.observers.filter(x => x !== o); }
notify(data: unknown) { this.observers.forEach(o => o.update(data)); }
}
Command: encapsula una solicitud como un objeto, permitiendo parametrizar clientes con distintas solicitudes, encolar solicitudes y deshacer operaciones.
interface Command { execute(): void; undo(): void; }
class AddTextCommand implements Command {
constructor(private doc: Document, private text: string) {}
execute() { this.doc.insert(this.text); }
undo() { this.doc.delete(this.text); }
}
Cómo se relacionan con SOLID
Los patrones no compiten con SOLID: lo implementan. La tabla muestra la conexión directa:
| Principio SOLID | Patrones que lo implementan naturalmente | |---|---| | SRP | Observer (separa el sujeto de los observadores) | | OCP | Strategy, Decorator, Observer (extendés sin modificar) | | LSP | Todos los patrones basados en interfaces (Strategy, Command, Adapter) | | ISP | Interface Segregation es la base de Adapter y Proxy | | DIP | Factory Method, Strategy, Observer (dependés de abstracciones) |
Si SOLID es el porqué, los patrones de diseño son el cómo.
Ejemplo trabajado: de IFs anidados a Strategy + Factory
Un sistema calcula el costo de envío según el tipo de entrega. La versión inicial es directa:
function calculateShipping(type: string, weight: number): number {
if (type === "standard") return weight * 1.5;
if (type === "express") return weight * 3.0 + 10;
if (type === "overnight") return weight * 5.0 + 25;
throw new Error(`Unknown shipping type: ${type}`);
}
Cuando el equipo agrega "same-day" y "international", la función crece con más ifs. El código viola OCP y SRP. Encima, testear cada rama requiere mockear todo el resto.
Refactorizando con Strategy + Factory Method:
// 1. Strategy: cada tipo de envío es una estrategia
interface ShippingStrategy {
calculate(weight: number): number;
}
class StandardShipping implements ShippingStrategy {
calculate(weight: number) { return weight * 1.5; }
}
class ExpressShipping implements ShippingStrategy {
calculate(weight: number) { return weight * 3.0 + 10; }
}
class OvernightShipping implements ShippingStrategy {
calculate(weight: number) { return weight * 5.0 + 25; }
}
// 2. Factory Method: crea la estrategia correcta según el tipo
class ShippingFactory {
private static strategies: Record<string, ShippingStrategy> = {
standard: new StandardShipping(),
express: new ExpressShipping(),
overnight: new OvernightShipping(),
};
static create(type: string): ShippingStrategy {
const strategy = this.strategies[type];
if (!strategy) throw new Error(`Unknown shipping type: ${type}`);
return strategy;
}
}
// 3. El cliente queda limpio
function calculateShipping(type: string, weight: number): number {
return ShippingFactory.create(type).calculate(weight);
}
Agregar "same-day" ahora es:
class SameDayShipping implements ShippingStrategy {
calculate(weight: number) { return weight * 8.0 + 50; }
}
// + registrarlo en ShippingFactory.strategies
Ni calculateShipping ni las estrategias existentes se modificaron. Cada estrategia se puede testear aislada. Eso es el poder de aplicar patrones con criterio.
Antes de refactorizar hacia un patrón, preguntate:
- ¿El problema actual es una instancia reconocible de un patrón que ya existe?
- ¿Aplicar el patrón va a reducir la cantidad de código que cambia cuando los requisitos cambien?
- ¿El equipo reconoce el patrón por nombre, o necesitarías un comentario de tres párrafos para explicarlo?
Si la respuesta es sí a las tres, usá el patrón. Si solo la primera es sí, quizás con una función bien nombrada alcanza.
Evidencia
- Gamma, Helm, Johnson, Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software es el texto canónico que introdujo los 23 patrones y las tres categorías. Cada patrón incluye intención, motivación, estructura, participantes y consecuencias.
- Refactoring.Guru, Design Patterns ofrece explicaciones visuales y ejemplos en múltiples lenguajes de cada patrón GoF, con diagramas de clases y código ejecutable. Es el recurso más accesible para aprender patrones desde cero.
- Robert C. Martin, The SOLID Principles contextualiza cómo los patrones de diseño implementan naturalmente los principios SOLID.
Fuentes citadas
- Gamma, Helm, Johnson, Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software (Primaria, 21-07-2026)
- Refactoring.Guru, Design Patterns (Práctica, 21-07-2026)
- Robert C. Martin, The SOLID Principles (Práctica, 21-07-2026)