Patrones de Diseño y Arquitectura
Clase 8
GoF, MVC, Clean Architecture y el arte de no sobre-ingeniar.
Objetivos de aprendizaje
Los patrones son vocabulario compartidoy soluciones probadas. Hoy aprendes a reconocerlos, aplicarlos y —lo más importante—a no usarlos cuando no hacen falta.
Qué son
Entender el origen GoF y por qué tener vocabulario común acelera al equipo.
GoF en práctica
Aplicar Singleton, Factory, Builder, Observer, Strategy y Adapter en TypeScript.
Arquitectura
Diferenciar MVC, MVVM y Clean Architecture y saber cuándo invertir en cada una.
Criterio
Detectar anti-patrones y aplicar YAGNI antes de añadir capas "por si acaso".
¿Qué son los patrones de diseño?
En 1994 la Gang of Four publicóDesign Patterns: 23 soluciones a problemas recurrentes del diseño orientado a objetos. Treinta años después siguen siendo el lenguaje común del oficio.
Vocabulario compartido
"Esto es un Observer" → todo el equipo entiende sin explicar 50 líneas de código.
Soluciones probadas
No reinventas la rueda: alguien ya enfrentó tu problema, lo nombró y lo documentó.
Mantenibilidad
Quien venga después reconocerá la forma y podrá modificar sin entender todo el sistema.
Flexibilidad controlada
Permiten cambiar partes del sistema sin romper otras… cuando la necesidad es real.
Aviso: los patrones son herramientas, no objetivos. Aplícalos cuando resuelven un problema real, no porque queden bien en el CV.
Las tres categorías GoF
| Categoría | Qué resuelve | Ejemplos |
|---|---|---|
| Creacionales | Cómo se crean los objetos | Singleton · Factory · Builder · Prototype |
| Estructurales | Cómo se componen objetos y clases | Adapter · Decorator · Facade · Composite |
| De comportamiento | Cómo interactúan y se comunican | Observer · Strategy · Command · State |
Esta clase cubre los 7 patrones que probablemente uses esta semana, no los 23. El resto vive en los libros del bloque "Para profundizar".
Practica reconocer el patrón
Lee cada escenario, elige el patrón que mejor encaja y comprueba. Cubre el Ejercicio 8.1 al completo.
#1Tu app soporta múltiples métodos de pago (tarjeta, PayPal, crypto) y pueden agregarse nuevos sin modificar el código existente.
Cada método de pago es un algoritmo intercambiable. Strategy permite añadir nuevos sin tocar el flujo principal (principio Open/Closed).
#2Tu app tiene un logger que debe ser el mismo en toda la aplicación.
Una única instancia compartida es exactamente lo que ofrece Singleton. Cuidado: úsalo solo para estado verdaderamente global.
#3Una API externa devuelve datos en formato diferente al que tu app espera.
Adapter traduce la interfaz incompatible (XML, formato legacy…) a la que tu sistema entiende, sin tocar al cliente original.
#4Cuando hay una compra: enviar email, actualizar inventario, generar factura y notificar al vendedor — todo de forma independiente.
Un evento con múltiples listeners desacoplados es el caso de uso clásico de Observer / EventEmitter.
#5Necesitas construir consultas SQL con múltiples cláusulas opcionales (where, limit, order…).
Builder permite construir objetos complejos paso a paso con una API encadenable y legible.
Para empezar con los patrones
Tres recursos suficientes para tener una base sólida antes de leer el libro original.
📚Material complementario▾
Singleton
Garantiza que una clase tenga una sola instancia y proporciona un punto global de acceso a ella.
class Database { private static instance: Database; private constructor() { console.log("Conectando a la BD..."); } static getInstance(): Database { if (!Database.instance) Database.instance = new Database(); return Database.instance; }}const a = Database.getInstance();const b = Database.getInstance();a === b // trueBuenos casos
Conexión a BD, logger, cache, registro de configuración cargada del entorno.
Cuidado
Es estado global disfrazado: hace tests difíciles. Si dudas, usa inyección de dependencias en su lugar.
Alternativa
En Node muchas veces basta con exportar una instancia desde un módulo: el propio sistema de módulos garantiza la unicidad.
Factory
Centraliza la creación de objetosdetrás de una interfaz común, ocultando el new al resto del código.
interface Notificacion { enviar(mensaje: string): void;}class Email implements Notificacion { ... }class SMS implements Notificacion { ... }class Push implements Notificacion { ... }class NotificacionFactory { static crear(tipo: "email"|"sms"|"push") { switch (tipo) { case "email": return new Email(); case "sms": return new SMS(); case "push": return new Push(); } }}NotificacionFactory.crear("email").enviar("Hola");Cuándo usarlo
Cuando la creación depende de un parámetro (tipo, config, entorno) y quieres aislarla del cliente.
Beneficio
Añadir un nuevo tipo (WhatsApp) toca solo la factory, no los 50 sitios que envían notificaciones.
Variantes
Factory Method (subclases deciden la clase concreta),Abstract Factory (familias de objetos relacionados).
Builder
Construye objetos complejos paso a paso, separando la construcción de la representación.
class QueryBuilder { private tabla = ""; private condiciones: string[] = []; private campos: string[] = ["*"]; private limite: number | null = null; from(t: string) { this.tabla = t; return this; } select(...c: string[]) { this.campos = c; return this; } where(c: string) { this.condiciones.push(c); return this; } limit(n: number) { this.limite = n; return this; } build(): string { /* arma el SQL */ }}new QueryBuilder() .from("usuarios") .select("nombre", "email") .where("edad > 18") .limit(10) .build();Cuándo brilla
Objetos con muchos parámetros opcionales: queries, requests HTTP, configuración de notificaciones.
Fluent interface
Cada método retorna this. El código se lee como una frase y el compilador valida cada paso.
Alternativa moderna
En TS muchas veces basta con un options object. Builder gana cuando hay orden, validaciones o estados intermedios.
Profundizar en creacionales
Cuidado con Singleton: tiene tantos defensores como detractores. Lee ambos lados.
Adapter
Traduce una interfaz incompatible a la que tu sistema entiende, sin tocar al cliente ni al servicio adaptado.
// API legacy: devuelve XMLclass APIVieja { obtenerDatosXML(): string { return "<user><n>Ana</n><e>25</e></user>"; }}// Tu app espera JSONclass AdapterUsuario { private vieja = new APIVieja(); obtener(): { nombre: string; edad: number } { const xml = this.vieja.obtenerDatosXML(); return { nombre: parse(xml, "n"), edad: +parse(xml, "e") }; }}Cuándo usarlo
APIs legacy, librerías de terceros con interfaces incómodas, migraciones incrementales (mantener el cliente intacto mientras cambias el backend).
Ya lo viste en
Los dialects de Sequelize (Postgres/SQLite/MySQL) — misma API JS sobre drivers nativos distintos.
Tip de diseño
Pon los Adapters en una capa propia (adapters/) para aislar el ruido del exterior — encaja con Clean Architecture, sección siguiente.
Decorator
Envuelve un objeto añadiendo comportamiento sin cambiar su interfaz. Apilable: varios decoradores se componen en cualquier orden.
interface Notificador { enviar(msg: string): void;}class EmailNotificador implements Notificador { enviar(m) { sendEmail(m); }}class ConLog implements Notificador { constructor(private wrapped: Notificador) {} enviar(m) { console.log("→ enviando:", m); this.wrapped.enviar(m); }}// Apilar decoradores:const n = new ConReintentos( new ConLog( new EmailNotificador()));Frente a herencia
La herencia fija el comportamiento en compile-time. Decorator lo compone en runtime y permite combinaciones que la jerarquía no anticipó.
Ya lo viste en
app.use(helmet()), cors(),rateLimit() en Express (Clase 7) — cada middleware decora la request. También los HOCs y custom hooks de React (Clase 5).
Decorators de TS
@Injectable(), @Controller() en NestJS/Angular — mismo nombre, misma idea: meta-decoración declarativa.
Facade
Ofrece una interfaz simple sobre un subsistema complejo. Esconde la coreografía interna y expone solo lo que el cliente necesita.
// Subsistema complejo (varios módulos)import { stripe } from "./payments";import { inventario } from "./stock";import { facturas } from "./billing";import { emailer } from "./email";// Facade: una sola puerta de entradaexport const tienda = { async comprar(usuario, items) { const cargo = await stripe.cobrar(...); await inventario.descontar(items); const pdf = await facturas.generar(cargo); await emailer.enviar(usuario, pdf); return { ok: true, cargo }; }};// El controlador solo hace:await tienda.comprar(req.user, req.body.items);Cuándo usarlo
Cuando un caso de uso típico requiere coordinar 4-5 módulos y todos los callers duplican esa coreografía.
Ya lo viste en
axios es una facade sobre fetch / XHR. La API de Prisma/Sequelize es una facade sobre el driver de la BD.
Facade vs Adapter
Adapter traduce una interfaz que ya existe.Facade inventa una interfaz nueva más simple sobre varias.
Cuidado
Una facade que crece sin control se convierte en God Object. Mantén una facade por caso de uso, no una por aplicación.
Profundizar en estructurales
Decorator, Facade, Adapter, Proxy y Composite componen el grupo. Estos recursos te dan ejemplos en TypeScript y comparativas entre patrones cercanos.
📚Material complementario▾
- DocsRefactoring.Guru — Structural patterns
- InteractivoAdapter en TypeScript (ejemplo completo)
- InteractivoDecorator en TypeScript (ejemplo completo)
- InteractivoFacade en TypeScript (ejemplo completo)
- DocsProxy vs Decorator vs Adapter — diferencias
- DocsTypeScript Decorators — Docs oficiales
- VideoChristopher Okhravi — Decorator pattern
- VideoChristopher Okhravi — Facade pattern
Observer
Define una dependencia uno-a-muchos: cuando un objeto cambia, todos sus observadores son notificados automáticamente.
class EventEmitter { private listeners = new Map<string, Listener[]>(); on(evento, cb) { /* registra */ } off(evento, cb) { /* elimina */ } emit(evento, data) { this.listeners.get(evento)?.forEach(cb => cb(data)); }}tienda.on("compra", e => email.enviar(e));tienda.on("compra", e => stock.descontar(e));tienda.on("compra", e => analytics.track(e));tienda.emit("compra", pedido);Lo que ganas
El emisor no conoce a sus oyentes. Añadir un nuevo listener no toca el código que dispara el evento.
Dónde lo ves cada día
DOM events (addEventListener), Node EventEmitter, RxJS, Redux store, hooks de React (useEffect + dependencias).
Riesgo
Olvidar off() al desmontar un componente → fugas de memoria y listeners zombi.
Observer en vivo
Suscribe y desuscribe listeners y dispara el evento. Verás que el emisor jamás se entera de quién está escuchando.
Listeners suscritos al evento compra:
Activa o desactiva listeners y dispara el evento. El emisor no cambia: los listeners se acoplan al canal, no al emisor.
Esperando emisión...Strategy
Encapsula algoritmos intercambiablesdetrás de una misma interfaz, para elegir uno u otro en tiempo de ejecución.
interface EstrategiaDescuento { calcular(precio: number): number;}class SinDescuento { calcular(p) { return p; } }class Porcentual { /* p * (1-x/100) */ }class BlackFriday { /* condicional */ }class Carrito { estrategia: EstrategiaDescuento; total() { return this.estrategia.calcular(this.subtotal()); }}Si tu código tiene…
un switch o cadena de if/else sobre un "tipo", casi seguro está pidiendo Strategy a gritos.
Casos típicos
Descuentos, métodos de pago, algoritmos de ordenamiento, políticas de retry, formateadores de exportación.
Strategy vs State
Estructuralmente iguales. La diferencia es semántica: Strategy lo elige el cliente; State cambia por sí mismo según las transiciones.
Cambia la estrategia, no el carrito
El carrito siempre llama a estrategia.calcular(). Alternar entre cuatro estrategias diferentes no cambia ni una línea del carrito.
Carrito
- Teclado mecánico89.00 €
- Ratón inalámbrico35.50 €
- Soporte monitor49.99 €
El carrito no sabe nada de descuentos: solo invoca estrategia.calcular().
Estrategia activa: Sin descuento
class SinDescuento implements EstrategiaDescuento {
calcular(p: number) { return p; }
}
class Descuento10 implements EstrategiaDescuento {
calcular(p: number) { return p * 0.9; }
}
class Descuento20 implements EstrategiaDescuento {
calcular(p: number) { return p * 0.8; }
}
class BlackFriday implements EstrategiaDescuento {
calcular(p: number) {
return p > 100 ? p * 0.5 : p * 0.7;
}
}
Profundizar en comportamiento
Observer y Strategy son la punta del iceberg. Command, State, Chain of Responsibility, Iterator, Mediator y Template Method completan el grupo.
📚Material complementario▾
- DocsRefactoring.Guru — Behavioral patterns
- InteractivoObserver en TypeScript (ejemplo completo)
- InteractivoStrategy en TypeScript (ejemplo completo)
- VideoStrategy vs State — Christopher Okhravi (video)
- DocsCommand pattern — Refactoring.Guru
- DocsChain of Responsibility (Express middlewares)
- DocsNode EventEmitter — Docs oficiales
- DocsRxJS — Observer pattern reactivo
Llevas semanas usando patrones
Cada framework que vimos en las Clases 3-7 está atravesado por patrones GoF. Una vez que les pones nombre, los reconoces en todos lados — y entiendes por qué las APIs se diseñaron así.
Clase 5 — React
Observer, Composite, Decorator, Strategy, Provider/DI.
Clase 6 — Express
Chain of Responsibility, MVC, Decorator, Strategy.
Clase 7 — Sequelize / JWT / Zod / Next
Active Record, Builder, Adapter, Strategy.
Las próximas dos diapositivas son un mapa: framework → patrón → dónde lo viste.
Frontend: React, Next.js, TypeScript
Si llegaste hasta aquí, ya escribiste estos patrones decenas de veces. Ahora les pones nombre.
| Herramienta | Patrón | Dónde lo viste |
|---|---|---|
| useState / useReducer | Observer | React se suscribe al estado y re-renderiza cuando cambia. |
| useEffect(deps) | Observer | Reactivas al cambio de cualquier valor del array de dependencias. |
| <Provider value={...}> | Singleton + Dependency Injection | Una instancia compartida que el subárbol consume vía useContext. |
| <Layout><Page /></Layout> | Composite | Componentes que contienen componentes; el árbol JSX es la estructura. |
| withAuth(Component) / hooks | Decorator | Añaden comportamiento (auth, logging) sin tocar el componente original. |
| React.createElement | Factory | El JSX que escribiste se compila a llamadas a esta factory. |
| render={(item) => <Row />} | Strategy | Render props: el padre decide cómo pintar cada elemento. |
| getServerSideProps / getStaticProps | Strategy | Next.js elige SSR o SSG según qué función exportes (Clase 7). |
| Discriminated unions en TS | Strategy | switch (action.type) en un reducer = strategy tipada. |
| MVVM mental model | MVVM | El componente con useState es el ViewModel: estado observable que la vista renderiza. |
Backend: Express, Sequelize, JWT, Zod, Node
Los frameworks del backend están aún más cargados de patrones que el frontend: APIs encadenables, middlewares y drivers intercambiables.
| Herramienta | Patrón | Dónde lo viste |
|---|---|---|
| app.use(middleware) | Chain of Responsibility | Cada middleware decide pasar la request al siguiente con next() (Clase 6). |
| helmet() / cors() / rateLimit() | Decorator | Envuelven la respuesta añadiendo headers o comportamiento (Clase 7). |
| routes / controllers / models | MVC | La estructura de carpetas de tu API de tareas es MVC clásico (Clase 6). |
| express.json() / urlencoded() | Strategy | Parsers intercambiables del body según el Content-Type. |
| EventEmitter (Node) | Observer | El patrón GoF en su forma más pura: .on() / .emit(). |
| Tarea.findAll({ where, include, limit, order }) | Builder | Sequelize construye SQL paso a paso a partir de un objeto encadenable (Clase 7). |
| sequelize.define(...) | Active Record | El modelo sabe cómo persistirse: tarea.save(), tarea.destroy(). |
| Sequelize dialects (pg, sqlite, mysql) | Adapter | Misma API JS para BDs distintas — Adapter sobre cada driver nativo. |
| new Sequelize(url) (instancia única) | Singleton | Una sola conexión exportada desde models/index.js. |
| jwt.sign(payload, secret, { algorithm }) | Strategy | HS256, RS256, ES256… algoritmos de firma intercambiables (Clase 7). |
| bcrypt vs argon2 vs scrypt | Strategy | Misma interfaz (hash/compare), distintos algoritmos. |
| z.object({...}).extend(...).refine(...) | Builder + Composite | Zod arma schemas componiendo otros schemas con API fluida (Clase 7). |
| git commit / git revert | Command + Memento | Cada commit es un comando reversible; el repo es la historia de snapshots (Clase 3). |
Cuando una API de un framework "se siente bien", suele ser porque aplica el patrón correcto sin que tengas que pensarlo.
Layered / N-tier
La arquitectura más antigua y todavía la más usada: el sistema se divide encapas horizontales y cada capa solo habla con la capa inmediatamente inferior.
┌──────────────────────────────┐│ Presentation (UI / API) │├──────────────────────────────┤│ Business / Service │├──────────────────────────────┤│ Persistence / Repository │├──────────────────────────────┤│ Database │└──────────────────────────────┘Regla: presentation NUNCA tocadirectamente persistence — pasapor la capa de servicios.Cuándo basta
La mayoría de las apps CRUD. Es lo que sale de la estructura del proyecto de la Clase 8 (routes/controllers/services/repositories).
Trampa típica
La capa de servicios se convierte en God Layer: meter ahí toda la lógica acaba siendo un Big Ball of Mud. Mantén dominio fuera.
Parientes
Es la base de la que parten MVC, Clean yHexagonal: todas son refinamientos sobre la idea de capas.
MVC — Modelo · Vista · Controlador
Separa la app en tres responsabilidades: datos,presentación yorquestación. La base de Express, Rails, Laravel y Spring.
// Controlador (orquesta)const tareasController = { listar: async (req, res) => { const tareas = await Tarea.findAll(); res.json(tareas); }, crear: async (req, res) => { const t = await Tarea.create(req.body); res.status(201).json(t); }};// Rutas (vista de la API)app.get ('/api/tareas', tareasController.listar);app.post('/api/tareas', tareasController.crear);Modelo
Datos y reglas de negocio. En la API de Clase 7 son los modelos Sequelize.
Vista
Lo que el usuario consume: HTML en SSR, JSON en una API, JSX en una SPA.
Controlador
Recibe la petición, llama al modelo y elige qué devolver. Debe serdelgado: la lógica vive en servicios.
Variantes
MVVM (View ↔ ViewModel, dominante en frontend reactivo).MVP. Hoy MVC en backend casi siempre se complementa con una capa de services.
Variantes de MVC: MVP y MVVM
Cuando la UI se vuelve compleja, MVC clásico se acorta. MVP yMVVM redistribuyen responsabilidades para que la vista sea más tonta y la lógica más testeable.
| Concepto | MVC | MVP | MVVM |
|---|---|---|---|
| Quién orquesta | Controller | Presenter | ViewModel |
| Vista ↔ lógica | Vista lee del modelo | Presenter empuja a la vista | Data-binding bidireccional |
| Vista testeable | Difícil | Sí (vista pasiva) | Muy fácil |
| Hábitat natural | APIs web, Rails, Express | Android clásico, WinForms | React, Vue, WPF, SwiftUI |
MVC
El controller maneja request → modelo → vista. La vista puede leer el modelo directamente. Dominante en backend.
MVP
La vista es pasiva: solo expone eventos. El presenter decide qué renderizar y llama a métodos de la vista (view.showError(...)).
MVVM
El ViewModel expone estado observable; la vista se re-renderiza sola cuando cambia. Es lo que haces cada día con useState en React.
Clean Architecture
Capas concéntricas donde las dependencias apuntan hacia adentro. La lógica de negocio no sabe que existen ni Express ni Prisma.
┌──────────────────────────────────────────────┐│ Frameworks & Drivers (Express, Prisma, …) ││ ┌────────────────────────────────────────┐ ││ │ Adaptadores (Controllers, Repos, DTO) │ ││ │ ┌──────────────────────────────────┐ │ ││ │ │ Casos de uso (Application) │ │ ││ │ │ ┌────────────────────────────┐ │ │ ││ │ │ │ Entidades (Domain rules) │ │ │ ││ │ │ └────────────────────────────┘ │ │ ││ │ └──────────────────────────────────┘ │ ││ └────────────────────────────────────────┘ │└──────────────────────────────────────────────┘Regla: las flechas de dependencia apuntansiempre hacia adentro. Lo interno NO conocea lo externo (DIP — Dependency Inversion).Entidades
Reglas de negocio puras: una Tarea sabe si está vencida, sin importar quién la pintó ni dónde se guarda.
Casos de uso
Orquestan entidades para cumplir intenciones del usuario:CrearTarea, CompletarTarea.
Adaptadores
Traducen entre el mundo exterior y el interior: HTTP → caso de uso, caso de uso → SQL. (¡Aquí vive el patrón Adapter!)
Cuándo invertir aquí
Proyectos con lógica de negocio compleja y larga vida útil. Para una API CRUD pequeña, Clean Architecture es sobre-ingeniería.
Hexagonal (Ports & Adapters)
Hermana de Clean Architecture, propuesta por Alistair Cockburn en 2005. La aplicación expone puertos (interfaces) y el mundo exterior se conecta mediante adaptersintercambiables.
┌────── REST adapter ──┐ │ ┌──────────┐ │CLI ────┼─[ port ]─│ │ │ │ │ APP │ │Tests ──┼─[ port ]─│ (dominio │ │ │ │ + casos │ │GraphQL─┼─[ port ]─│ uso ) │ │ │ └────┬─────┘ │ └─[ port: Repository ]───┘ │ ┌────────┼────────┐ ▼ ▼ ▼ Postgres Mongo InMemoryEl dominio solo conoce puertos.Los adapters son enchufables.Idea central
La lógica de negocio no sabe si la entrada vino por HTTP, CLI o un test. Tampoco sabe si los datos viven en Postgres o en memoria.
Testing real
Cambiar PostgresRepository por InMemoryRepositorybasta para tener tests rápidos del dominio sin BD.
Hexagonal vs Clean
Misma intención (DIP). Hexagonal pone el foco enpuertos; Clean en capas. En la práctica se mezclan: muchos proyectos llaman "Hexagonal" a su Clean.
Monolito vs Microservicios
No es una cuestión de moda: es una decisión organizacional. Microservicios cambian un problema técnico (acoplamiento) por uno operativo (distribuir, observar, desplegar).
| Concepto | Monolito | Monolito modular | Microservicios |
|---|---|---|---|
| Despliegue | Un solo artefacto | Un artefacto, módulos aislados | N pipelines, N entornos |
| Tx entre módulos | ACID local | ACID local | Saga / eventual |
| Escalado | Todo o nada | Todo o nada | Por servicio |
| Coste operativo | Bajo | Bajo | Alto (obs, K8s, red, …) |
| Equipos | 1–2 equipos | 2–5 equipos | Muchos equipos autónomos |
Empieza así
Monolith first (Fowler). Empieza con un monolito modular bien diseñado. Extrae servicios cuando duela, no antes.
Falacia común
"Microservicios = código más limpio". No: el código se ordena con módulos. Microservicios se justifican por escalado y autonomía de equipos.
Ley de Conway
"Las organizaciones diseñan sistemas que reflejan su estructura de comunicación". Tu arquitectura acabará pareciéndose a tu organigrama.
Event-Driven · CQRS · Event Sourcing
Patrones de arquitectura para sistemas distribuidos o de alta escritura/lectura. No son obligatorios — son herramientas cuando el dominio lo pide.
Event-Driven Architecture
Los servicios se comunican publicando eventos en un broker (Kafka, RabbitMQ, SQS) en vez de llamarse entre sí por HTTP.
OrderService │ publish "OrderPlaced" ▼┌──Kafka────┐ │ │ │ ▼ ▼ ▼Email Stock InvoiceEs el patrón Observer elevado a nivel de sistema.
CQRS
Command Query Responsibility Segregation. Separa el modelo deescritura del de lectura. Cada uno se optimiza para su carga.
POST /orders ──► Write Model (Postgres) ▼ proyecciónGET /orders ───► Read Model (ElasticSearch)Útil cuando lees ≫ escribes (feeds, dashboards).
Event Sourcing
En vez de guardar el estado actual, guardas lasecuencia de eventos que llevó a él. El estado se reconstruye replicando eventos.
AccountOpenedMoneyDeposited(100)MoneyWithdrawn(30)→ saldo: 70Auditoría perfecta gratis. Compleja para queries directas.
Regla: empieza sin nada de esto. Adóptalo solo cuando el dominio lo justifique (alta escala, auditoría legal, equipos autónomos). YAGNI también aplica aquí.
Profundizar en arquitectura
Los patrones son tácticos; la arquitectura es estratégica. Estos recursos te ayudan a entender la diferencia.
📚Material complementario▾
- ArtículoThe Clean Architecture — Uncle Bob (artículo original)
- ArtículoHexagonal Architecture — Alistair Cockburn
- ArtículoMartin Fowler — GUI Architectures (MVC, MVP, MVVM…)
- DocsDomain-Driven Design Reference (Eric Evans)
- VideoThe Clean Architecture — Robert C. Martin (video)
- VideoLeo Códigos — Antes de implementar Clean Architecture en NextJS
- VideoLeo Códigos — ¿Cómo encaja la UI en Clean Architecture?
- VideoLeo Códigos — Aprende todo sobre use cases (Clean Architecture)
- VideoLeo Códigos — Capa de presentación con NextJS (Clean Architecture)
- ArtículoMicroservices — Martin Fowler (artículo fundacional)
- ArtículoMonolithFirst — Martin Fowler
- ArtículoCQRS — Martin Fowler
- ArtículoEvent Sourcing — Martin Fowler
- ArtículoBuilding Event-Driven Microservices — Adam Bellemare (libro)
- VideoPorts & Adapters — Cockburn (charla)
- ArtículoModular Monolith — Kamil Grzybek
Anti-patrones y sobre-ingeniería
Conocer los patrones es la mitad. La otra mitad es sabercuándo no aplicarlos.
| Anti-patrón | Qué es | Solución |
|---|---|---|
| God Object | Una clase que sabe y hace todo | Responsabilidad única (SRP) |
| Spaghetti Code | Todo conectado con todo | Separar capas y módulos |
| Copy-Paste Programming | Duplicar en vez de abstraer | Extraer funciones / DRY |
| Premature Optimization | Optimizar antes de medir | "Make it work, make it right, make it fast" |
| Golden Hammer | Misma herramienta para todo | Elegir la adecuada al problema |
YAGNI
You Ain't Gonna Need It. No añadas funcionalidad "por si acaso". Cuando llegue la necesidad real, llegará con información que ahora no tienes.
Regla de oro
Si dudas si necesitas un patrón, probablemente no lo necesitas. Espera a que el dolor sea real y entonces refactoriza.
Refactor en vivo: Strategy + Observer
El código Antes es el del Ejercicio 8.4: un if/else con email y analytics duplicados. Alterna a Después y observa cómo Strategy y Observer separan responsabilidades.
function procesarPago(tipo, monto, datos) { if (tipo === "tarjeta") { if (datos.numero.length !== 16) return { exito: false, error: "Numero invalido" }; console.log(`Cobrando $${monto} a tarjeta ${datos.numero}`); console.log(`Email: Pago de $${monto} con tarjeta`); console.log(`Analytics: pago_tarjeta, $${monto}`); return { exito: true, transaccion: "TXN-" + Date.now() }; } else if (tipo === "paypal") { if (!datos.email.includes("@")) return { exito: false, error: "Email invalido" }; console.log(`Cobrando $${monto} via PayPal a ${datos.email}`); console.log(`Email: Pago de $${monto} con PayPal`); console.log(`Analytics: pago_paypal, $${monto}`); return { exito: true, transaccion: "PP-" + Date.now() }; }}// 1) Strategy — cada método encapsuladoclass PagoTarjeta { validar(d) { return d.numero?.length === 16; } cobrar(monto, d) { return { id: "TXN-" + Date.now(), canal: "tarjeta", monto }; }}class PagoPaypal { validar(d) { return d.email?.includes("@"); } cobrar(monto, d) { return { id: "PP-" + Date.now(), canal: "paypal", monto }; }} // 2) Observer — notificaciones desacopladasconst bus = new EventEmitter();bus.on("pago", (e) => emailService.confirmar(e));bus.on("pago", (e) => analytics.track("pago", e)); // 3) Orquestadorfunction procesarPago(estrategia, monto, datos) { if (!estrategia.validar(datos)) return { exito: false, error: "Datos invalidos" }; const r = estrategia.cobrar(monto, datos); bus.emit("pago", r); return { exito: true, transaccion: r.id };}Ejecuta un pago para ver la traza...En el código «Antes», añadir crypto exige editar el if/else y duplicar las llamadas a email y analytics. En el «Después» basta con crear PagoCrypto y registrarlo.
📚Material complementario▾
- DocsAntiPatterns — capítulo gratuito (Brown et al.)
- ArtículoA Philosophy of Software Design — John Ousterhout
- ArtículoYAGNI — Martin Fowler
- DocsRefactoring (2.ª ed.) — Martin Fowler (catálogo online)
- VideoLeo Códigos — Unlocks dependency injection (matar el Singleton-fobia)
- VideoLeo Códigos — Mejora los mensajes de error de tu API
Resumen de la Clase 8
| Concepto | Definición |
|---|---|
| Patrón de diseño | Solución probada a un problema recurrente de diseño |
| GoF | Gang of Four — los 4 autores del libro clásico de 1994 |
| Singleton | Una única instancia con punto de acceso global |
| Factory | Encapsula la creación de objetos detrás de una interfaz |
| Builder | Construcción paso a paso con API encadenable |
| Observer | Notifica automáticamente a varios oyentes ante un evento |
| Strategy | Algoritmos intercambiables en tiempo de ejecución |
| Adapter | Traduce interfaces incompatibles sin tocar al cliente |
| MVC | Separación Modelo · Vista · Controlador |
| Clean Architecture | Capas concéntricas con dependencias hacia adentro |
| YAGNI | No implementes lo que aún no necesitas |
Ejercicio 8.1: Identificar patrones
Cinco escenarios, un patrón por cada uno. Usa el interactivo de la sección 2 para autoevaluarte; abajo tienes la rúbrica.
Ver solución
- Strategy — cada método de pago es intercambiable.
- Singleton — una sola instancia del logger.
- Adapter — convierte el formato externo al interno.
- Observer — múltiples listeners independientes.
- Builder — query construida paso a paso.
Ejercicio 8.2: Implementar un Factory
Crea un sistema de formas geométricas usando Factory.
Requisitos
- • Interfaz
Formaconarea(),perimetro(),describir() - • Implementa
Circulo,Rectangulo,Triangulo - •
FormaFactory.crear(tipo, dimensiones)instancia la forma correcta - • Lanza error claro si el tipo no existe
Bonus
- ⭐ Añadir
Cuadradosin tocar nada salvo la factory - ⭐ Validar dimensiones con Zod antes de construir
- ⭐ Tests con Vitest para cada forma
Ejercicio 8.3: Observer en la práctica
Sistema de notificaciones para una tienda online. Tres eventos, tres listeners independientes.
Eventos
- •
usuario:registro - •
pedido:creado - •
pedido:enviado
Listeners
- •
EmailService - •
AnalyticsService - •
InventarioService
Simula el flujo completo: registro → compra → envío. Comprueba que cada listener se ejecuta solo cuando le corresponde.
Ejercicio 8.4: Refactorizar con patrones
El interactivo de la sección 6 contiene el código original. Identifica al menos dos patrones aplicables y reescríbelo.
Ver pista
Hay un if/else sobre un "tipo" que crece linealmente: olor aStrategy.
Las llamadas a email y analytics se duplican en cada rama: olor a Observer.
Proyecto: refactor con arquitectura
Toma la API de tareas de las Clases 6-7 y refactorízala aplicando patrones y separación de responsabilidades. Misma funcionalidad, mejor estructura.
Objetivo
- • Capas claras: routes / controllers / services / repositories
- • Al menos 2 patrones de diseño aplicados
- • API funciona idénticamente a la versión anterior
- • Cero sobre-ingeniería
Patrones candidatos
- • Repository para aislar Sequelize
- • Strategy para política de orden/filtro
- • Observer para hooks post-creación
- • Factory para el cliente HTTP de tests
Estructura sugerida
Cada carpeta tiene una sola razón para cambiar.
src/├── routes/ (rutas Express)├── controllers/ (HTTP → caso de uso)├── services/ (lógica de negocio)├── repositories/ (acceso a BD)├── middleware/ (auth, validar, errors)└── utils/ (helpers puros)controllers/
Delgados. Solo extraen datos del request, llaman al service y formatean la respuesta.
services/
Donde vive la lógica de negocio. No tocan Express ni Sequelize directamente.
repositories/
Único punto que habla con Sequelize. Cambiar de Sequelize a Prisma toca solo esta carpeta (¡eso es el patrón Adapter en acción!).
Criterios de evaluación
Checklist
- ☑ Separación clara en capas
- ☑ Al menos 2 patrones aplicados correctamente
- ☑ API se comporta idéntica a la versión anterior
- ☑ Código más legible y mantenible
- ☑ No hay sobre-ingeniería gratuita
- ☑ README justifica los patrones elegidos
Bonus
- ⭐ Tests por capa con doubles (mocks de repository)
- ⭐ Inyección de dependencias real (no Singletons)
- ⭐ Manejo centralizado de errores con middleware
- ⭐ ADR (Architecture Decision Record) en
docs/
Para profundizar
Bibliografía mínima si quieres construir criterio propio sobre patrones y arquitectura. No leas todo: elige uno por categoría.
📖 Libros
- • Design Patterns — GoF
- • Head First Design Patterns — Freeman
- • Clean Architecture — Robert C. Martin
- • A Philosophy of Software Design — Ousterhout
- • Refactoring (2.ª ed.) — Fowler
- • Domain-Driven Design — Eric Evans
🎥 Videos y charlas
- • Okhravi — Design Patterns (playlist)
- • Uncle Bob — The Clean Architecture
- • Sandi Metz — All the Little Things
- • Mary Poppendieck — The Tyranny of the Plan
- • Greg Young — 8 Lines of Code
🔗 Sitios y referencias
- • refactoring.guru
- • sourcemaking.com
- • martinfowler.com
- • blog.cleancoder.com
- • herbertograca.com (Software Architecture Chronicles)
💻 Código y práctica
- •
kamranahmedse/design-patterns-for-humans - •
torokmark/design_patterns_in_typescript - • Refactoring Katas — Emily Bache
- • exercism.io — track de TypeScript
- •
jasonrudolph/programming-motherf**ker(currículo libre)
📚Material complementario▾
- ArtículoDesign Patterns (libro GoF) — referencia y diagramas
- ArtículoClean Architecture — Robert C. Martin (artículo)
- ArtículoA Philosophy of Software Design (Ousterhout)
- DocsRefactoring catalog (Martin Fowler)
- ArtículoSoftware Architecture Chronicles
- InteractivoRefactoring Katas — Emily Bache
- DocsTypeScript Design Patterns (torokmark)
- VideoChristopher Okhravi — Design Patterns playlist
- VideoLeo Códigos — Canal completo (Clean Architecture en español)
- VideoLeo Códigos — ¿Cuál es la mejor estructura de carpetas en Clean Architecture?
- VideoLeo Códigos — Manejo de errores en React con Clean Architecture
Tienes vocabulario nuevo
Saber nombrar lo que ya hacías es la mitad del oficio. La otra mitad es sabercuándo no aplicar nada. En la próxima clase llevamos todo esto a un proyecto integrador end-to-end.