ISW · Arquitectura
🔁 Guía de Aula Invertida

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.

Paso 01 · El problema

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 →

Paso 02 · Capas

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.

Paso 03 · La dependencia que duele

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).

Paso 04 · Un puerto

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.

Paso 05 · Invertir

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.

Paso 06 · Lo que se gana

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.

Paso 07 · Cuándo no hace falta

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.

Diagrama en vivoPaso 1 / 7
PASO 1: la pantalla usa las reglas, y las reglas usan la base. Cada flecha dice «depende de».

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.

¿Quieres armarlo tú, desde la carpeta vacía?

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.

Node.js — antes: reglas y SQL mezclados
// 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);
}
Node.js — después: el dominio declara el puerto
// 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;
  }
}
Node.js — los adaptadores cumplen el puerto
// 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
Quién importa a quié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.

Clasificación

¿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érminoQué esEn EventoUAB
Capa de presentaciónLo que el usuario ve y toca.El formulario de reserva y las rutas que lo reciben.
DominioEntidades y reglas del negocio, sin detalles técnicos.Club, Espacio, Reserva y «no se solapan».
InfraestructuraLos detalles técnicos que pueden cambiar.La base de datos y el envío de correos.
PuertoLo que el dominio necesita de afuera, descrito sin tecnología.RepositorioDeReservas.
AdaptadorCódigo de afuera que cumple un puerto con una tecnología concreta.RepoSqlite, RepoEnMemoria.
Conexión con el modelo de dominio

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.

Emparejamiento

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.

0 / 0
actividades correctas en toda la guía
Selección múltiple

¿Qué significa «invertir una dependencia» en el ejemplo de EventoUAB?

Verdadero o falso

Después de invertir la dependencia, las reglas del dominio ya no llaman a la base de datos cuando hay que guardar una reserva.

Selección múltiple

Bienestar pide pasar EventoUAB de SQLite a PostgreSQL. Con el puerto ya definido, ¿qué hay que escribir?

Selección múltiple

¿En cuál de estos casos separar en capas con puertos probablemente sea más ceremonia que ayuda?

Completar

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.

Análisis abierto

Capas, dependencias y puertos de un proyecto real.

Cheat Sheet

TérminoIdea clave
CapaGrupo de código que cambia por la misma razón.
DominioEntidades 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.
PuertoLo que el dominio necesita de afuera, descrito sin tecnología.
AdaptadorCó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ácticaLos imports apuntan hacia el dominio.
BeneficioCambiar de tecnología sin tocar las reglas, y probarlas sin base de datos.
Cuándo noPrototipos 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.