Actores y direcciones: driving vs. driven
Primario o secundario, driving o driven: la distinción no es por dónde entran los datos, sino quién inicia la conversación. El test que conduce tu aplicación, el correo que solo responde y los casos que engañan a todo el mundo.
Son las 8:03 de la mañana en la cafetería y ocurren dos conversaciones casi al mismo tiempo. Primera: la barista toca «Confirmar pedido» en la tablet y la aplicación despierta, verifica el pedido y lo confirma. Segunda: la aplicación, por su cuenta, le pide al servicio de correo que avise al cliente que su encargo va en camino. Misma frontera, mismo hexágono… pero las flechas apuntan en direcciones opuestas. Reproduce ambas conversaciones y fíjate en quién da el primer paso en cada una:
Confirmar pedido (driving)
- Barista (tablet)Alguien de afuera necesita algo: la barista toca «Confirmar pedido» y saca a la aplicación del reposo.
- AdaptadoresHandler HTTPEl adaptador traduce el POST en una llamada agnóstica al caso de uso.
- DominioConfirmarPedido.ejecutarEl caso de uso orquesta la operación dentro del hexágono.
- DominioPedido.confirmar()La entidad aplica su regla: un pedido sin líneas no se confirma.
Avisar por correo (driven)
- DominioConfirmarPedido.ejecutarEsta vez es la aplicación quien inicia la conversación: necesita avisar al cliente.
- PuertosNotificadorDePedidos (puerto)El núcleo habla por su contrato, sin saber quién está del otro lado.
- AdaptadoresSmtpNotificadorDePedidosEl adaptador traduce el contrato agnóstico al dialecto del correo.
- Servidor SMTPEl servidor de correo solo responde: nunca inicia nada.
Esa diferencia de dirección tiene nombre propio en el patrón —driving y driven— y es, con distancia, el concepto que más se malinterpreta de la arquitectura hexagonal. Este módulo existe para que no te pase.
Actores: quién habla con tu aplicación
Cockburn dibujó su hexágono con dos regiones. Garrido de Paz lo resume así sobre el diagrama canónico:
“Actors on the left/top side are Drivers, or Primary Actors. The interaction is triggered by the actor.”
“Actors on the right/bottom side are Driven Actors, or Secondary Actors. The interaction is triggered by the application.”
Un actor primario (driving) es cualquiera que necesita algo de la aplicación y la saca del reposo: la barista en la tablet, una app de delivery que envía pedidos, un script nocturno, otro sistema. Un actor secundario (driven) es cualquiera a quien la aplicación necesita para completar su trabajo: la base de datos que guarda los pedidos, el servicio de correo que avisa, la pasarela que cobra. Los primarios están a un lado del hexágono por convención de dibujo; lo que los define no es la posición, sino la frase clave de ambas citas: quién dispara la interacción.
La pregunta correcta: ¿quién inicia la conversación?
El error clásico es traducir driving/driven como “entrada/salida”, y es
un mal mapa. Piénsalo con el repositorio de la cafetería: cuando el caso
de uso llama a buscar(id), los datos del pedido entran a la
aplicación desde la base de datos. ¿Convierte eso a PostgreSQL en un
actor “de entrada”? No: la conversación la inició el núcleo, la base de
datos solo respondió. Los datos fluyen en ambos sentidos en los dos
lados del hexágono; lo que nunca cambia es quién habló primero.
Por eso la única pregunta confiable es: ¿quién inicia la conversación? Si el de afuera toca la puerta, es driving. Si el núcleo marca el número, es driven. Grábatela: el minijuego de este módulo está diseñado para tenderte todas las trampas conocidas.
Adaptadores driving y driven: usar vs. implementar
La dirección de la conversación también parte en dos a los adaptadores del módulo anterior, y la distinción técnica es elegante. Citas textuales de Hexagonal Me:
“A driver adapter uses a driver port interface, converting a specific technology request into a technology agnostic request to a driver port.”
“A driven adapter implements a driven port interface, converting the technology agnostic methods of the port into specific technology methods.”
Ahí está la asimetría que dejamos pendiente: el adaptador driving usa un puerto del núcleo; el adaptador driven implementa un puerto del núcleo. Míralo en el código, frente a frente:
El handler HTTP
/// Traduce un POST /pedidos/42/confirmar en una llamada
/// agnóstica al caso de uso. Usa el núcleo; no lo implementa.
async fn confirmar_pedido(
State(app): State<Aplicacion>,
Path(id): Path<PedidoId>,
) -> Respuesta {
match app.confirmar_pedido.ejecutar(id) {
Ok(()) => Respuesta::sin_contenido(),
Err(error) => Respuesta::desde(error),
}
}No implementa ningún trait del dominio: recibe un POST, lo traduce y
llama al caso de uso.
El caso de uso
/// Caso de uso: confirmar un pedido y avisar al exterior.
pub struct ConfirmarPedido<R: PedidoRepository, N: NotificadorDePedidos> {
repositorio: R,
notificador: N,
}
impl<R: PedidoRepository, N: NotificadorDePedidos> ConfirmarPedido<R, N> {
pub fn ejecutar(&self, id: PedidoId) -> Result<(), ErrorDeConfirmacion> {
let mut pedido = self.repositorio.buscar(id)?; // puerto driven
pedido.confirmar()?; // regla del dominio
self.repositorio.guardar(&pedido)?; // puerto driven
self.notificador.pedido_confirmado(&pedido)?; // puerto driven
Ok(())
}
}Declara con genéricos los dos puertos driven que necesita; el repositorio Postgres no llama a nadie, espera a que el núcleo lo invoque a través del trait que implementa.
Cuenta las flechas: el handler llama al caso de uso (entra al hexágono), el caso de uso llama al repositorio y al notificador (sale del hexágono). Tres conversaciones, un solo hilo de ejecución, y cada frontera cruzada por un contrato.
El test es un actor primario
Aquí el patrón muestra su carta ganadora. Recuerda la intención de
Cockburn del módulo 1: que la aplicación pueda ser conducida “por
usuarios, programas, tests automatizados o scripts batch” por igual. Un
test que invoca ConfirmarPedido::ejecutar no es un ciudadano de
segunda: es un actor primario con todas las de la ley, que entra por el
mismo puerto que el handler HTTP. En el artículo original, Cockburn
señala para cada lado su sustituto natural de pruebas: el arnés de tests
como adaptador driving (“FIT is the natural test adapter for primary
actors”) y los objetos simulados como adaptadores driven (“mock objects
are the natural test adapter for secondary actors”).
La simetría es preciosa: puedes sustituir al humano por un test en el lado driving, y a la base de datos por un doble en memoria en el lado driven. Compáralo con la primera conversación de las 8:03 — la flecha entra por el mismo lugar:
- Test automatizadoEl test inicia la conversación, igual que la barista en la tablet.
- DominioConfirmarPedido.ejecutarMismo puerto de entrada que el handler HTTP: el núcleo ejecuta exactamente el mismo código sin distinguir quién lo conduce.
De eso vivirá el módulo de testing, más adelante en el curso.
Los casos que engañan
Tres trampas clásicas, para que llegues advertido al minijuego:
El cron nocturno. “Es infraestructura, seguro es driven”. Falso: el scheduler que dispara el cierre de caja a las 11 pm inicia la conversación igual que la barista. La pregunta no es qué tan fierro se ve, sino quién habla primero.
La cola de mensajes. No es driving ni driven “en general”: depende de la conversación. El consumidor que recibe pedidos de la app de delivery dispara casos de uso — driving. El publicador con el que el núcleo emite «pedido confirmado» es invocado por la aplicación — driven. Misma tecnología, dos roles.
El sistema externo grande. La pasarela de pagos impone respeto, pero el tamaño no da la iniciativa. Cuando el núcleo le pide cobrar, es driven; cuando su webhook avisa que el cobro se acreditó, esa conversación la inicia ella: driving. Un mismo sistema puede jugar en ambos lados del hexágono, en conversaciones distintas.
Minijuego: driving o driven
Diez dependencias del sistema de la cafetería. Para cada una, la única pregunta que importa: ¿quién inicia la conversación? Las trampas de la sección anterior están todas adentro, más alguna sorpresa.
Lo que viene
Ya sabes quién habla con quién y en qué dirección. La siguiente pregunta
es de fontanería: si el caso de uso solo conoce traits, ¿en qué momento
y en qué lugar se decide que PedidoRepository será Postgres y
NotificadorDePedidos será SMTP? La respuesta tiene nombre propio
—composition root— y un único lugar legítimo donde vivir. Es el tema
del próximo módulo.
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