Arquitectura por capas y por qué invertir una dependencia
EventoUAB ya tiene historias, casos de uso y un modelo de dominio con Club, Espacio y Reserva. Falta decidir dónde vive cada cosa en el código. Si las reglas del negocio quedan mezcladas con la pantalla y con el SQL, cualquier cambio de tecnología las arrastra. Esta guía muestra cómo ordenar el código en capas y qué significa «invertir una dependencia» para que el modelo de dominio quede protegido en el centro.
👁 — vistas · Ing. Roy Carrasco, Facultad de Ingeniería de Sistemas · UAB
Scrollea para empezar: el diagrama de EventoUAB se arma solo, paso a paso, y las flechas cambian de sentido mientras lees.
Todo en un mismo archivo
La primera versión de «Reservar espacio» funciona. Pero está escrita como suele salir cuando hay apuro: la pantalla llama a una función que valida la regla («dos reservas no pueden solaparse») y, en esa misma función, escribe el SQL que consulta y guarda en la base.
Dibujemos quién usa a quién. Cada flecha significa «depende de»: la pantalla depende de las reglas, y las reglas dependen de la base →
Agrupar por motivo de cambio
Una capa es un grupo de código que cambia por la misma razón. La presentación cambia cuando cambia la pantalla. La infraestructura (base de datos, correo, servicios externos) cambia cuando cambia la tecnología. En el medio, el dominio: las entidades del modelo (Club, Espacio, Reserva) y las reglas del negocio, que cambian solo cuando cambia el negocio mismo.
Las reglas dependen de la base
Hay una flecha roja: el código del dominio importa la librería de la base. Tiene tres consecuencias concretas. Si Bienestar pide pasar de SQLite a PostgreSQL, hay que abrir y volver a probar las reglas. Para probar «no se pueden solapar dos reservas» hay que levantar una base real. Y lo que cambia seguido (la tecnología) arrastra a lo que cambia poco y más importa (las reglas).
El dominio declara lo que necesita
En vez de preguntar «¿cómo guardo esto en SQLite?», las reglas dicen qué necesitan, en su propio idioma: RepositorioDeReservas, con dos operaciones, hayConflicto(...) y guardar(...). Esa descripción es un puerto. No tiene una sola palabra de SQL y vive del lado del dominio, dentro del núcleo que ahora se marca con línea punteada.
La base cumple el puerto, y la flecha se da vuelta
Ahora es la base la que se adapta: RepoSqlite implementa el puerto, y es su código el que importa al dominio. La flecha roja desapareció y apareció una nueva que apunta hacia el núcleo. Eso es invertir la dependencia: el principio DIP dice que las reglas de alto nivel no deben depender de los detalles de bajo nivel, y que ambos deben depender de una abstracción, acá el puerto.
Ojo con una confusión frecuente: en la ejecución, las reglas siguen llamando a la base (piden guardar). Lo que se invirtió es la dependencia del código, quién importa a quién, no el flujo de datos.
Un segundo implementador: sin base de datos
Como el dominio solo conoce el puerto, se puede escribir otro adaptador que guarde las reservas en una lista en memoria. Con él, las reglas se prueban en milisegundos, sin levantar ninguna base. Y pasar a PostgreSQL ya no es reescribir nada: es sumar un tercer adaptador y elegir cuál se usa al arrancar la aplicación.
No es una receta para todo
Si EventoUAB fuera un prototipo de una sola pantalla para una feria de fin de semana, tres capas y un puerto serían pura ceremonia. Conviene separar cuando aparece alguna señal: reglas que no son triviales, pruebas que cuestan mucho, o un cambio de tecnología a la vista. La regla para recordar, cuando sí aplica: los imports apuntan hacia el dominio.
Del diagrama al código
Así se ve la misma función antes y después. Los ejemplos están en Node.js (JavaScript con módulos ES), pero la idea es idéntica en Python, Java o C#: lo que cambia es el lugar desde donde se importa cada cosa.
Esta sección muestra el código ya hecho. Para crear el proyecto paso a paso (comandos, carpetas, archivos, pruebas y la base SQLite), sigue el taller de Node.js.
// reservas.js — todo junto
import Database from "better-sqlite3"; // las reglas ya dependen de SQLite
export function reservarEspacio(clubId, espacioId, inicio, fin) {
const db = new Database("eventouab.db");
const ocupado = db
.prepare("SELECT 1 FROM reserva WHERE espacio_id = ? AND inicio < ? AND fin > ?")
.get(espacioId, fin, inicio);
if (ocupado) { // la regla, mezclada con la consulta
throw new Error("Espacio ocupado en ese horario");
}
db.prepare("INSERT INTO reserva VALUES (?, ?, ?, ?)")
.run(clubId, espacioId, inicio, fin);
}
// dominio/reserva.js (no importa nada de afuera)
export class Reserva {
constructor(clubId, espacioId, inicio, fin) {
if (fin <= inicio) throw new Error("El fin debe ser posterior al inicio");
this.clubId = clubId;
this.espacioId = espacioId;
this.inicio = inicio;
this.fin = fin;
}
}
// dominio/puertos.js (lo que el dominio NECESITA, sin decir cómo)
export class RepositorioDeReservas {
hayConflicto(espacioId, inicio, fin) { throw new Error("no implementado"); }
guardar(reserva) { throw new Error("no implementado"); }
}
// dominio/reservarEspacio.js (el caso de uso recibe el puerto)
import { Reserva } from "./reserva.js"; // solo importa dentro del dominio
export class ReservarEspacio {
constructor(repo) { this.repo = repo; }
ejecutar(clubId, espacioId, inicio, fin) {
const reserva = new Reserva(clubId, espacioId, inicio, fin);
if (this.repo.hayConflicto(espacioId, inicio, fin)) {
throw new Error("Espacio ocupado en ese horario");
}
this.repo.guardar(reserva);
return reserva;
}
}
// infraestructura/repoSqlite.js (importa el puerto: la flecha apunta al dominio)
import { RepositorioDeReservas } from "../dominio/puertos.js";
export class RepoSqlite extends RepositorioDeReservas {
constructor(db) { super(); this.db = db; }
hayConflicto(espacioId, inicio, fin) {
const fila = this.db
.prepare("SELECT 1 FROM reserva WHERE espacio_id = ? AND inicio < ? AND fin > ?")
.get(espacioId, fin.toISOString(), inicio.toISOString());
return fila !== undefined;
}
guardar(r) {
this.db
.prepare("INSERT INTO reserva VALUES (?, ?, ?, ?)")
.run(r.clubId, r.espacioId, r.inicio.toISOString(), r.fin.toISOString());
}
}
// pruebas/repoEnMemoria.js (otro adaptador: un arreglo, sin base de datos)
import { RepositorioDeReservas } from "../dominio/puertos.js";
export class RepoEnMemoria extends RepositorioDeReservas {
constructor() { super(); this.reservas = []; }
hayConflicto(espacioId, inicio, fin) {
return this.reservas.some(
(r) => r.espacioId === espacioId && r.inicio < fin && r.fin > inicio);
}
guardar(reserva) { this.reservas.push(reserva); }
}
// La misma regla funciona con cualquiera de los dos:
const caso = new ReservarEspacio(new RepoEnMemoria()); // en las pruebas
const caso = new ReservarEspacio(new RepoSqlite(db)); // en producción
En el archivo «antes», las reglas importan better-sqlite3. En el «después», dominio/ no importa nada de afuera; infraestructura/ importa al dominio. Si en algún momento ves un import del dominio hacia la base, el framework o la pantalla, la dependencia volvió a apuntar al lado equivocado.
¿Cada import respeta que las dependencias apunten hacia el dominio?
dominio/reserva.js hace import Database from "better-sqlite3" para validar si el espacio existe.
infraestructura/repoSqlite.js importa RepositorioDeReservas desde el dominio.
presentacion/rutas.js importa y llama a ReservarEspacio, que está en el dominio.
dominio/reservarEspacio.js importa express para leer los datos del formulario.
dominio/puertos.js importa la entidad Reserva desde dominio/reserva.js.
dominio/club.js importa repoSqlite para guardar el club directamente.
El vocabulario de la guía
Distintos libros usan nombres distintos para lo mismo (capas, hexagonal, limpia, onion). No importan los nombres, importa la dirección de las flechas. Estos son los términos que usamos acá.
| Término | Qué es | En EventoUAB |
|---|---|---|
| Capa de presentación | Lo que el usuario ve y toca. | El formulario de reserva y las rutas que lo reciben. |
| Dominio | Entidades y reglas del negocio, sin detalles técnicos. | Club, Espacio, Reserva y «no se solapan». |
| Infraestructura | Los detalles técnicos que pueden cambiar. | La base de datos y el envío de correos. |
| Puerto | Lo que el dominio necesita de afuera, descrito sin tecnología. | RepositorioDeReservas. |
| Adaptador | Código de afuera que cumple un puerto con una tecnología concreta. | RepoSqlite, RepoEnMemoria. |
Esto conecta con el modelo de dominio de la guía anterior: las entidades que dibujaste (Club, Espacio, Reserva) son exactamente lo que vive en el centro de este diagrama. Todo lo demás existe para protegerlas de los cambios de tecnología.
Une cada término con su descripción.
Actividades de repaso
Un repaso integrador de todo el módulo. Cada actividad se corrige al instante; tu progreso se guarda automáticamente en este navegador.
¿Qué significa «invertir una dependencia» en el ejemplo de EventoUAB?
Después de invertir la dependencia, las reglas del dominio ya no llaman a la base de datos cuando hay que guardar una reserva.
Bienestar pide pasar EventoUAB de SQLite a PostgreSQL. Con el puerto ya definido, ¿qué hay que escribir?
¿En cuál de estos casos separar en capas con puertos probablemente sea más ceremonia que ayuda?
Repasemos el diagrama de EventoUAB.
1. Las entidades y las reglas del negocio viven en la capa de .
2. Lo que el dominio necesita de afuera se declara como un .
3. RepoSqlite, que cumple ese puerto con SQLite, es un .
4. Con la dependencia invertida, los imports apuntan siempre hacia el .
🎯 Reto: las capas de tu propio proyecto
Con el producto que está construyendo tu equipo, escribe en dos o tres líneas por punto: 1) las tres capas con un archivo o módulo real de cada una, 2) una regla del negocio de tu dominio y en qué archivo vive hoy, 3) una tecnología que podría cambiar (base, framework, servicio externo) y qué tendrías que tocar si cambiara, 4) un puerto que le pondrías al dominio, con las dos o tres operaciones que necesita, 5) una razón honesta por la que, en tu caso, todavía no valdría la pena separarlo.
Capas, dependencias y puertos de un proyecto real.
- Cada capa tiene al menos un archivo o módulo concreto, no solo una etiqueta.
- La regla de negocio elegida es del negocio (por ejemplo, «un espacio no se reserva dos veces») y no una pieza técnica.
- El cambio de tecnología se analiza con las flechas: dices qué archivos importarían ese detalle hoy y cuántos habría que tocar.
- El puerto está descrito en el idioma del dominio (guardar, buscar, hay conflicto), sin mencionar SQL ni librerías.
- La razón para no separar todavía es una señal real (tamaño, tiempo de vida, equipo) y no «me da pereza».
Cheat Sheet
| Término | Idea clave |
|---|---|
| Capa | Grupo de código que cambia por la misma razón. |
| Dominio | Entidades y reglas del negocio; el centro que se protege. |
| Dependencia | «Importa a»: si A importa a B, un cambio en B puede romper a A. |
| Puerto | Lo que el dominio necesita de afuera, descrito sin tecnología. |
| Adaptador | Código de afuera que cumple un puerto con una tecnología concreta. |
| Invertir la dependencia (DIP) | Las reglas de alto nivel no dependen de detalles; ambos dependen de una abstracción. |
| Regla práctica | Los imports apuntan hacia el dominio. |
| Beneficio | Cambiar de tecnología sin tocar las reglas, y probarlas sin base de datos. |
| Cuándo no | Prototipos descartables y sistemas sin reglas relevantes: más ceremonia que ayuda. |
¿Sabías que...?
🔤 La «D» de SOLID
El principio de inversión de dependencias es la quinta y última letra del acrónimo SOLID, un conjunto de cinco principios de diseño orientado a objetos que Robert C. Martin popularizó desde los años 90.
⬡ ¿Por qué «hexagonal»?
Alistair Cockburn bautizó «arquitectura hexagonal» (o de puertos y adaptadores) a esta misma idea en 2005. El hexágono no tiene nada de especial: solo dejaba espacio para dibujar varios puertos por todos lados.
⬇️ La arquitectura clásica de tres capas
La versión tradicional dibujaba presentación, lógica y datos apilados, con flechas siempre hacia abajo, justo el «problema» del primer paso. Invertir la dependencia de la capa de datos es lo que la volvió más flexible.