Rust y Arquitectura Hexagonal: código limpio sin compromisos

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Rust y Arquitectura Hexagonal: escribiendo código que no da miedo cambiar

\n\n

Cada desarrollador se ha enfrentado al menos una vez a la situación en la que un pequeño cambio en la base de datos o un cambio de framework HTTP se convierte en una refactorización infernal. La lógica de negocio se entrelaza con los detalles de implementación, y el código se convierte en "espagueti". Para Rust, con su enfoque en seguridad y rendimiento, tal mezcla es un verdadero crimen.

\n\n

La solución a este problema es la Arquitectura Hexagonal (también conocida como Ports and Adapters). En este artículo, analizaremos cómo aplicar este patrón en Rust, utilizando el poder del sistema de tipos y los traits. Aprenderá cómo aislar la lógica de dominio del mundo exterior, escribir código testeable y cambiar fácilmente los controladores de base de datos o servidor web. Esta es una habilidad obligatoria para cualquiera que quiera construir sistemas complejos y mantenibles en Rust.

\n\n

¿Qué es la Arquitectura Hexagonal? (Muy breve)

\n\n

Imagine un hexágono. En el centro está su lógica de negocio (dominio). En los bordes están los "puertos" (interfaces) a través de los cuales el dominio se comunica con el mundo exterior. Y en el exterior están los "adaptadores" (implementaciones concretas) que se conectan a los puertos.

\n\n

Idea clave: el dominio no sabe nada sobre cómo se guardan los datos o se envían las solicitudes. Solo conoce los puertos — traits con métodos. Los adaptadores (por ejemplo, un repositorio PostgreSQL o un controlador HTTP) implementan estos traits.

\n\n

En Rust, este concepto encaja perfectamente gracias a los traits (análogo a interfaces) y los genéricos. El compilador verifica todas las conexiones en tiempo de compilación, no en tiempo de ejecución.

\n\n

Estructura del proyecto en Rust

\n\n

Para empezar, crearemos una estructura de carpetas que separe visualmente el dominio de la infraestructura:

\n\n
src/\n├── domain/          # Lógica de negocio pura (¡sin dependencias!)\n│   ├── mod.rs\n│   ├── models.rs    # Entidades y Value Objects\n│   └── services.rs  # Servicios que trabajan con puertos\n├── ports/           # Traits (interfaces) para el mundo exterior\n│   ├── mod.rs\n│   └── repositories.rs\n├── adapters/        # Implementaciones de puertos (bases de datos, HTTP, etc.)\n│   ├── mod.rs\n│   ├── postgres_repo.rs\n│   └── in_memory_repo.rs  # Para pruebas\n├── main.rs          # Ensamblaje de la aplicación (Wiring)\n└── lib.rs           # API pública de la biblioteca
\n\n

Paso 1: Dominio — el corazón del sistema

\n\n

Empecemos con un modelo simple de usuario. En el dominio no hay ninguna dependencia de bibliotecas externas (ni siquiera de serde, a menos que la lógica de negocio lo requiera).

\n\n
// src/domain/models.rs\npub struct User {\n    pub id: String,\n    pub name: String,\n    pub email: String,\n}\n\nimpl User {\n    pub fn new(id: String, name: String, email: String) -> Self {\n        // Aquí puede haber validación\n        Self { id, name, email }\n    }\n}
\n\n

Ahora definamos un servicio de dominio. Trabajará con usuarios, pero no sabrá de dónde vienen.

\n\n
// src/domain/services.rs\nuse crate::ports::repositories::UserRepository;\n\npub struct UserService {\n    repo: Box<dyn UserRepository>,\n}\n\nimpl UserService {\n    pub fn new(repo: Box<dyn UserRepository>) -> Self {\n        Self { repo }\n    }\n\n    pub fn create_user(&mut self, name: String, email: String) -> Result<User, String> {\n        let id = uuid::Uuid::new_v4().to_string(); // ¿Ya es una dependencia? Sí, pero es un detalle\n        let user = User::new(id, name, email);\n        self.repo.save(&user)?;\n        Ok(user)\n    }\n\n    pub fn get_user(&self, id: &str) -> Result<Option<User>, String> {\n        self.repo.find_by_id(id)\n    }\n}
\n\n

Importante: Usamos Box<dyn UserRepository> para despacho dinámico. En Rust, esto es normal para aplicaciones complejas. Para casos simples, se pueden usar genéricos.

\n\n

Paso 2: Puertos — contratos para el exterior

Blogs

Book Recommendations