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:
- DominioDominio
PedidoConfirmarPedidoDefine el puerto PedidoRepository y lo usa, sin conocer ninguna implementación concreta.
- PuertosPuertos
PedidoRepositoryNotificadorDePedidos - AdaptadoresAdaptadores
PostgresPedidoRepositoryNotificadorSmtp - InfraestructuraInfraestructura
main()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.
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:
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:
- Config::desde_entorno()La única fuente de decisiones tecnológicas: URLs, credenciales, puerto.
- AdaptadoresPostgresPedidoRepository::conectar / NotificadorSmtp::nuevoAquí, y solo aquí, se nombra la tecnología concreta.
- DominioConfirmarPedido::new(...)El grafo de objetos se ensambla una sola vez, por constructor.
- 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
- Mark Seemann, Composition Root (ploeh blog, 2011): blog.ploeh.dk/2011/07/28/CompositionRoot
- Alistair Cockburn, Hexagonal Architecture (HaT Technical Report, 2005): alistair.cockburn.us/hexagonal-architecture
- Alistair Cockburn, Configurable Dependency (2012), referenciado en los recursos de Hexagonal Me de Juan Manuel Garrido de Paz: jmgarridopaz.github.io/content/resources.html
- Master Hexagonal Architecture in Rust (How to Codeit): howtocodeit.com/guides/master-hexagonal-architecture-in-rust