Domain-Driven Design en proyectos Rust: cuando los tipos se convierten en sus aliados
¿Alguna vez ha escrito una aplicación Rust en la que la lógica de negocio se ahogaba en un mar de estructuras, traits y macros? ¿Esa sensación familiar de que, al mirar el código, no puede decir de inmediato qué hace tal o cual función en el contexto del dominio? Si es así, bienvenido al club. Domain-Driven Design (DDD) está diseñado para resolver este problema, y Rust con su sistema de tipos es el terreno ideal para implementar los principios de DDD.
En este artículo no vamos a resumir el libro de Evans. En su lugar, mostraré cómo aplicar Domain-Driven Design en proyectos Rust con ejemplos reales. Aprenderá cómo, mediante tipos envoltorio, arquitectura modular y estructuras expresivas, hacer que el código de dominio sea limpio, seguro y comprensible. ¡Allá vamos!
Por qué Rust y DDD son la pareja ideal
Domain-Driven Design exige un aislamiento estricto de la lógica de dominio respecto a la infraestructura. Rust proporciona herramientas potentes para ello:
- Sistema de tipos — permite modelar reglas de negocio a nivel del compilador. ¿Un error? El compilador no lo permitirá.
- Propiedad y préstamo — garantizan que los agregados y entidades no sean modificados accidentalmente en un lugar inesperado.
- Sistema de módulos — separa claramente la capa de dominio, la infraestructura y la aplicación.
Pero también hay trampas. Rust no perdona los objetos «gordos» con un montón de estado mutable. Por eso, DDD en Rust requiere un enfoque de modelado diferente al de, por ejemplo, Java o C#.
Idea clave: haga que los estados inválidos sean irrepresentables
En DDD buscamos que las reglas de negocio estén codificadas en la propia estructura de datos. En Rust esto se convierte en un mantra: «Si un estado no es válido, haga que sea imposible crearlo».
En lugar de escribir:
struct User { name: String, email: String, age: u8,}Creamos tipos que ya en tiempo de compilación garantizan la corrección:
pub struct Email(String);pub struct Age(u8);pub struct UserName(String);
pub struct User { name: UserName, email: Email, age: Age,}¿Parecen simples envoltorios? En realidad, este es el primer paso hacia DDD en Rust, donde los tipos se convierten en documentación y guardianes de los invariantes.
Modelamos el dominio: Value Object y Entity
Los conceptos centrales de DDD son Entity (entidad) y Value Object (objeto-valor). En Rust se implementan de manera diferente.
Value Object en Rust: inmutabilidad e igualdad por valor
Un Value Object no tiene identidad. Dos objetos son iguales si sus campos son iguales. En Rust encaja perfectamente con estructuras que usan #[derive(PartialEq)] y campos inmutables.
use std::fmt;
#[derive(Debug, Clone, PartialEq)]pub struct Money { amount: i64, currency: Currency,}
#[derive(Debug, Clone, PartialEq)]pub enum Currency { USD, EUR, RUB,}
impl Money { pub fn new(amount: i64, currency: Currency) -> Self { // Regla de negocio: el dinero no puede ser negativo assert!(amount >= 0, "Amount must be non-negative"); Self { amount, currency } }
pub fn add(&self, other: &Money) -> Result<Self, String> { if self.currency != other.currency { return Err("Cannot add different currencies".into()); } Ok(Self::new(self.amount + other.amount, self.currency.clone())) }}Observe: Money es inmutable. Cualquier operación devuelve un nuevo objeto. Esto se alinea con la filosofía de DDD en Rust: nada de setters, solo métodos que crean nuevas instancias.