Saltar al contenido
Módulo 4 de 7Intermedio

Inyección de dependencias y composition root

El núcleo depende de contratos, pero alguien tiene que elegir los adaptadores concretos. La configurable dependency de Cockburn, la inversión de dependencias y el único lugar legítimo para cablearlo todo: el main().

Nuestro sistema de pedidos de cafetería ya quedó bien repartido: la entidad Pedido y sus reglas viven en el núcleo, el caso de uso ConfirmarPedido orquesta la operación, y dos puertos — PedidoRepository y NotificadorDePedidos — declaran lo que el núcleo necesita del mundo exterior sin nombrar ninguna tecnología. Pero queda una pregunta incómoda: si el caso de uso solo conoce contratos, ¿quién escribe la línea que construye el repositorio de Postgres? Alguien tiene que hacerlo. Este módulo trata de quién, de dónde y de por qué solo ahí.

Configurable dependency: la bisagra del patrón

Alistair Cockburn le puso nombre a la mecánica que hace funcionar todo el hexágono: la configurable dependency, que describe como

“a dependency of an object on an interface […] enabling runtime flexibility through interface-based design rather than concrete technology bindings.”

Es decir: un objeto no depende de otra clase concreta, sino de una interfaz —en Rust, de un trait— y la implementación real se decide en el arranque (o en el test). El caso de uso declara “necesito algo que guarde pedidos”; que ese algo sea Postgres, SQLite o un HashMap en memoria es una decisión configurable, tomada fuera del hexágono. Cada puerto del módulo 2 era, en el fondo, una dependencia configurable esperando a que alguien la configurara.

La flecha que cambia de sentido: Dependency Inversion

Esta mecánica es la aplicación directa del Dependency Inversion Principle (la “D” de SOLID): los módulos de alto nivel —el dominio— no deben depender de módulos de bajo nivel —la infraestructura—; ambos deben depender de abstracciones. En la arquitectura en capas del módulo 1, la flecha de dependencia apuntaba del negocio hacia los datos, y por eso todo terminaba hablando el idioma de la base de datos. En hexagonal, la flecha de compilación se invierte — búscala en el diagrama: la que va de la infraestructura al dominio, la única que en capas apuntaba al revés:

  • DominioDominioPedidoConfirmarPedido

    Define el puerto PedidoRepository y lo usa, sin conocer ninguna implementación concreta.

  • PuertosPuertosPedidoRepositoryNotificadorDePedidos
  • AdaptadoresAdaptadoresPostgresPedidoRepositoryNotificadorSmtp
  • InfraestructuraInfraestructuramain()axumPostgreSQL
  • Infraestructura → Dominio — permitida: implementa PedidoRepository e importa al dominio para hacerlo — nunca al revés
  • Dominio → Puertos — permitida: el dominio define y usa su propio puerto
  • Adaptadores → Puertos — permitida: un adaptador implementa el trait del puerto
  • Adaptadores → Dominio — prohibida: una entidad del dominio no deriva un trait del framework
  • Infraestructura → Adaptadores — permitida: axum y PostgreSQL son la tecnología detrás del adaptador

La inversión no es un truco de sintaxis: es la razón por la que el crate del dominio compila sin tener a Postgres ni al framework web en sus dependencias. El contrato lo dicta quien lo necesita, no quien lo sirve.

Inyección por constructor

¿Y cómo recibe el caso de uso sus dependencias configurables? De la forma más aburrida y más robusta posible: por constructor. El objeto no fabrica a sus colaboradores; los recibe ya hechos, como parámetros, y solo sabe de ellos que cumplen el contrato.

confirmar_pedido.rs — el caso de uso solo conoce contratosDominio
pub struct ConfirmarPedido<R: PedidoRepository, N: NotificadorDePedidos> {
    repositorio: R,
    notificador: N,
}

impl<R: PedidoRepository, N: NotificadorDePedidos> ConfirmarPedido<R, N> {
    /// Inyección por constructor: los colaboradores llegan de fuera.
    pub fn new(repositorio: R, notificador: N) -> Self {
        Self { repositorio, notificador }
    }

    pub fn ejecutar(&self, id: PedidoId) -> Result<Pedido, ErrorDePedido> {
        let mut pedido = self.repositorio.buscar(id)?;
        pedido.confirmar()?; // la regla de negocio vive en la entidad
        self.repositorio.guardar(&pedido)?;
        self.notificador.notificar_confirmacion(&pedido);
        Ok(pedido)
    }
}

Fíjate en lo que no aparece: ninguna URL de conexión, ningún cliente SMTP, ningún use de un crate de infraestructura. Los genéricos R y N dicen “cualquier cosa que implemente el puerto me sirve”. Ese es el caso de uso completo — y sin embargo, todavía no funciona: nadie ha decidido qué R y qué N serán. Falta el último eslabón.

El composition root: un solo lugar para cablearlo todo

Mark Seemann bautizó ese eslabón en su artículo Composition Root (2011). Su definición:

“A (preferably) unique location in an application where modules are composed together.”

Y debe ubicarse, en sus palabras, “as close as possible to the application’s entry point” — en nuestro caso, el main(). La idea clave del patrón es que el resto del código se limita a declarar lo que necesita:

“all the application code relies solely on Constructor Injection […] but is never composed. Only at the entry point is the entire object graph finally composed.”

Todo el mundo pide por constructor; nadie construye. Solo en el punto de entrada se ensambla, una única vez, el grafo completo de objetos. Así se ve el nuestro:

main.rs — el composition root de la cafeteríaInfraestructura
fn main() -> Result<(), ErrorDeArranque> {
    // 1. Configuración: la única fuente de decisiones tecnológicas.
    let config = Config::desde_entorno()?;

    // 2. Adaptadores concretos: aquí (y solo aquí) se nombra la tecnología.
    let repositorio = PostgresPedidoRepository::conectar(&config.url_base_datos)?;
    let notificador = NotificadorSmtp::nuevo(&config.smtp);

    // 3. Composición: el grafo de objetos se ensambla una sola vez.
    let confirmar_pedido = ConfirmarPedido::new(repositorio, notificador);

    // 4. Arranque: el adaptador driving recibe el caso de uso ya listo.
    ServidorHttp::en_puerto(config.puerto).servir(confirmar_pedido)
}

Cuatro pasos y un orden que no es casual — recórrelos uno a uno:

  1. Config::desde_entorno()La única fuente de decisiones tecnológicas: URLs, credenciales, puerto.
  2. AdaptadoresPostgresPedidoRepository::conectar / NotificadorSmtp::nuevoAquí, y solo aquí, se nombra la tecnología concreta.
  3. DominioConfirmarPedido::new(...)El grafo de objetos se ensambla una sola vez, por constructor.
  4. AdaptadoresServidorHttp::en_puerto(...).servir(...)El adaptador driving recibe el caso de uso ya listo y arranca.

Al compilar, Rust monomorfiza el genérico en un tipo concreto (ConfirmarPedido<PostgresPedidoRepository, NotificadorSmtp>) sin coste alguno en tiempo de ejecución; mañana, otro main() —o un test— puede ensamblar ConfirmarPedido con un repositorio en memoria sin tocar una sola línea del núcleo. Si prefieres decidir el adaptador en tiempo de ejecución (por ejemplo, según la configuración), Rust ofrece la alternativa de los trait objects (Box<dyn PedidoRepository>), a cambio de una indirección: son dos formas válidas de la misma dependencia configurable, un trade-off y no un dogma, como documenta la guía Master Hexagonal Architecture in Rust.

Compruébalo tú: intercambia el adaptador detrás del puerto y vigila la caja del núcleo — no va a moverse.

El caso de uso ConfirmarPedido<R, N> habla con el puertoPuertosPedidoRepository y no cambia con ninguna de estas opciones:

PostgresPedidoRepository
Conecta a PostgreSQL real: el arranque de producción.
SqlitePedidoRepository
Archivo local, útil en desarrollo.
PedidoRepositoryEnMemoria
HashMap en RAM, se pierde al cerrar — el que usaremos en el módulo 5.

Seemann añade una restricción que suele sorprender:

“Only applications should have Composition Roots. Libraries and frameworks shouldn’t.”

Una biblioteca expone piezas componibles; no decide cómo se ensamblan. El único código con derecho a conocer todas las implementaciones concretas a la vez es el punto de entrada de una aplicación.

La fuga de construcción

El antipatrón opuesto merece nombre propio. Si dentro de ConfirmarPedido::ejecutar apareciera esta línea:

let repositorio = PostgresPedidoRepository::conectar("postgres://...")?;

el caso de uso volvería a saber qué base de datos usas, el crate del dominio tendría que depender del driver de Postgres, y cada prueba volvería a necesitar una base de datos real. Una sola línea deshace los tres módulos anteriores. La señal de alarma es sencilla: si ves un ::new() o un ::conectar() de un adaptador concreto fuera del composition root, hay una fuga de construcción. La construcción se escapa hacia el interior con disfraces variados —un helper “inocente” que crea la conexión, un singleton global, una factory dentro del dominio— y el diagnóstico es siempre el mismo: alguien que no es el main() está tomando decisiones de tecnología.

Minijuego: ¿dónde vive esta línea?

Cada tarjeta describe una línea o acción de código del sistema de la cafetería. Decide si pertenece al composition root (el main() que cablea tecnología) o al interior del hexágono (reglas y contratos que no conocen adaptadores).

Lo que viene

Con todo el cableado concentrado en un solo lugar, cambiar de adaptadores es una decisión de arranque — y un test no es más que otro arranque, uno que elige adaptadores de mentira. En el próximo módulo cobramos la recompensa: sustituir Postgres y SMTP por test doubles (fakes, stubs, spies y mocks) para probar ConfirmarPedido en milisegundos.

Fuentes

Práctica

Practica lo aprendido

Tarjetas de repaso

Evaluación del módulo