¿Por qué separar el negocio de la tecnología?
El problema antes que el patrón: qué duele cuando la UI, la base de datos y el framework se enredan con las reglas de negocio, y qué quería resolver Alistair Cockburn en 2005.
Antes de dibujar un solo hexágono, conviene sentir el problema. Imagina que mantienes el sistema de pedidos de una cafetería que también surte encargos grandes para oficinas. Un día te piden un cambio pequeño: “cuando un pedido supere los $2,000, aplica un 10% de descuento”. Abres el código y la regla no está en ninguna parte… y está en todas: un pedazo vive en el handler HTTP, otro en una consulta SQL y otro en la plantilla que pinta el total del ticket. Cambiar una regla de negocio te obliga a tocar la interfaz, la base de datos y el framework a la vez. Eso es acoplamiento, y es el enemigo que este curso te enseña a aislar.
Este pedido de cafetería nos va a acompañar durante todo el curso.
Módulo a módulo lo iremos refinando hasta convertirlo en una aplicación
hexagonal completa, con un dominio Pedido, un caso de uso
ConfirmarPedido y un par de contratos con el mundo exterior. Pero
primero hay que entender por qué la forma “natural” de organizar el
código nos metió en este lío.
La arquitectura en capas y sus dependencias transitivas
La respuesta clásica fue la arquitectura en capas: presentación arriba, negocio en medio, datos abajo, cada capa dependiendo de la de abajo. El diagrama se ve ordenado, pero esconde una trampa. Recorre la cadena paso a paso y observa la flecha que nadie dibujó:
- PresentaciónLa pantalla del ticket declara una sola dependencia: la capa de negocio.
- NegocioEl negocio, a su vez, depende de la capa de datos para leer y guardar.
- DatosAl fondo viven el esquema de tablas, el ORM y los tipos del driver.
- Presentación depende de DatosLa flecha que nadie dibujó: la presentación termina dependiendo de los datos aunque nunca lo declaró.
A esa filtración se le llama dependencia transitiva: si A depende de B y B depende de C, entonces A depende de C aunque nunca lo haya declarado. Por eso un cambio en el esquema de la base de datos sacude el edificio completo, piso por piso, hasta llegar a la pantalla — y la capa “de negocio” acaba hablando el idioma de la infraestructura.
El costo se paga en las pruebas
El síntoma más caro aparece al probar. La regla del descuento quedó enredada con la petición HTTP y la consulta SQL, así que hazte la pregunta: ¿qué necesitas levantar para verificar que el 10% se aplica bien? Recorre la utilería completa:
- Levantar BDArrancar un servidor de base de datos solo para esta prueba.
- Crear tablasReproducir el esquema completo antes de poder insertar nada.
- Sembrar pedidoInsertar un pedido con líneas que sumen más de $2,000.
- Petición HTTP falsaFabricar una petición con el pedido_id correcto.
- EjecutarRecién entonces: ejecutar la función y mirar el resultado del if.
Cinco pasos de utilería para una sola línea de negocio.
La cadena de consecuencias es siempre la misma: las pruebas se vuelven lentas y frágiles, así que se ejecutan menos; como se ejecutan menos, los cambios dan miedo; como los cambios dan miedo, se posponen; y el código se degrada un poco más con cada parche tímido. Todo por una flecha de dependencia que apunta en la dirección equivocada.
La misma regla, sin rehenes
Ahora sí, el código. Mira la misma regla dos veces, frente a frente: a la izquierda, tomada como rehén por HTTP y SQL; a la derecha, viviendo en un tipo propio del dominio, con el descuento como un método sobre ese tipo:
Con rehenes
// La "regla de negocio" necesita HTTP y SQL solo para existir.
fn total_con_descuento(req: &HttpRequest, db: &Conexion) -> f64 {
let id: u64 = req.query("pedido_id").parse().unwrap();
let filas = db.query("SELECT precio, cantidad FROM lineas WHERE pedido_id = ?", id);
let subtotal: f64 = filas.iter().map(|f| f.precio * f.cantidad).sum();
if subtotal > 2000.0 { subtotal * 0.90 } else { subtotal }
}Sin rehenes
pub struct LineaDePedido {
pub precio_unitario: f64,
pub cantidad: u32,
}
pub struct Pedido {
lineas: Vec<LineaDePedido>,
}
impl Pedido {
pub fn subtotal(&self) -> f64 {
self.lineas
.iter()
.map(|l| l.precio_unitario * f64::from(l.cantidad))
.sum()
}
/// Regla de negocio: 10% de descuento arriba de $2,000.
pub fn total(&self) -> f64 {
let subtotal = self.subtotal();
if subtotal > 2000.0 { subtotal * 0.90 } else { subtotal }
}
}En el panel acoplado, la única línea que es negocio de verdad es la del
if: todo lo demás es tecnología que tomó a la regla como rehén. Probar
la regla del panel derecho, en cambio, cuesta una línea: construyes un
Pedido en memoria y afirmas sobre total(). Sin servidor, sin tablas,
sin peticiones falsas. Fíjate en que todavía no dibujamos ningún hexágono ni
usamos vocabulario nuevo: solo movimos la regla a un tipo que habla el
idioma del negocio. Ese gesto —proteger un núcleo que no sabe nada de la
tecnología que lo rodea— es exactamente lo que el patrón de este curso
convierte en disciplina.
Queda una pregunta incómoda, claro: si Pedido ya no conoce la base de
datos, ¿quién guarda los pedidos? ¿Y quién avisa al cliente cuando su
pedido se confirma? Responderla con precisión es el trabajo de los
próximos módulos.
La intención original: Cockburn, 2005
Alistair Cockburn documentó el patrón de la arquitectura hexagonal (Ports and Adapters) en 2005 precisamente para atacar este problema. Su intención, textual:
“Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.”
Es decir: que la aplicación pueda ser conducida por igual por un humano, otro programa, un test automatizado o un script batch, y que pueda desarrollarse y probarse aislada de sus dispositivos y bases de datos definitivos. La regla de frontera que lo hace posible también es suya: el código del interior no debe filtrarse hacia el exterior —y nada del exterior (tipos del ORM, peticiones HTTP, DTOs del framework) debe filtrarse hacia el interior.
Dentro de esa frontera queda el núcleo: solo lo que importa para el problema de negocio, “sin referencias a ninguna tecnología, framework o dispositivo del mundo real”, como lo resume Juan Manuel Garrido de Paz, colaborador de Cockburn. Es el código que sobrevive aunque mañana cambies de framework web o de base de datos.
Nota el orden de las ideas: Cockburn no partió de un diagrama bonito, sino de un dolor operativo —no poder probar sin infraestructura, no poder cambiar de tecnología sin tocar el negocio— y diseñó el patrón hacia atrás desde ahí. Por eso este curso empezó por el dolor y no por el hexágono.
Minijuego: Detective de acoplamiento
Hora de entrenar el ojo. Te presentamos descripciones de fragmentos de código del sistema de la cafetería: decide cuáles mezclan negocio con tecnología y cuáles son negocio puro. La pista de siempre: pregúntate qué necesitarías levantar para probar ese fragmento. Si la respuesta incluye una base de datos, un framework o una red, ya tienes al culpable.
Lo que viene
En los próximos módulos le pondremos nombre a cada pieza de la
solución: los puertos (los contratos que el núcleo ofrece y
necesita, como PedidoRepository para guardar pedidos o
NotificadorDePedidos para avisar al cliente), los adaptadores (las
implementaciones concretas para cada tecnología) y la dirección de las
conversaciones (driving vs. driven). Por ahora quédate con la idea
central del curso: el negocio no debería saber qué base de datos
usas — y cuando lo sabe, lo pagas en cada prueba y en cada cambio.
Fuentes
- Alistair Cockburn, Hexagonal Architecture (HaT Technical Report, 2005): alistair.cockburn.us/hexagonal-architecture
- Juan Manuel Garrido de Paz, Ports and Adapters Architecture (Hexagonal Me): jmgarridopaz.github.io/content/hexagonalarchitecture.html
- Wikipedia, Hexagonal architecture (software) (historia y relación con otras arquitecturas): en.wikipedia.org/wiki/Hexagonal_architecture_(software)