Patrones de diseño en Rust: no solo una moda, sino una necesidad
Cuando hablamos de Rust, muchos imaginan un sistema de tipos, el borrow checker y la ausencia de un recolector de basura. Pero detrás de esta fachada de rigor se esconde una poderosa herramienta para el diseño de arquitectura. Los patrones de diseño (design patterns) en Rust no son solo un homenaje a las tradiciones de Java o C++. Son una forma de hacer que el compilador trabaje para ti, no en tu contra.
En este artículo, analizaremos por qué los patrones clásicos de GoF (Gang of Four) se ven diferentes en Rust, cuáles son realmente útiles y cuáles son antipatrones. Y lo más importante: veremos código real que puedes aplicar de inmediato en tu proyecto.
¿Por qué Rust cambia las reglas del juego?
Los patrones de diseño clásicos a menudo resuelven problemas relacionados con la gestión de memoria, la herencia y el polimorfismo. Rust, en cambio, ofrece un enfoque completamente diferente:
- Propiedad y préstamo — en lugar de recolección de basura, gestionas la memoria explícitamente. Los patrones que en otros lenguajes resuelven el problema de "quién posee el objeto" en Rust se vuelven triviales o simplemente innecesarios.
- Rasgos (Traits) — el reemplazo de la herencia. Rust no admite la herencia clásica de la POO, pero los traits brindan flexibilidad sin los problemas de la herencia en diamante.
- Enums y coincidencia de patrones — una herramienta poderosa que reemplaza muchos patrones relacionados con el estado y la ramificación.
- Estilo funcional — iteradores, closures y combinadores permiten escribir código conciso sin patrones explícitos.
Por lo tanto, muchos patrones en Rust no se ven como "clase e interfaz", sino como trait + struct + enum. Veamos ejemplos concretos.
Patrón Builder: construyendo objetos complejos
El patrón Builder es uno de los más populares en Rust. Es ideal para crear objetos con una gran cantidad de parámetros, especialmente cuando algunos de ellos son opcionales.
Problema: constructor con 10 parámetros
Imagina que necesitas crear una configuración de un cliente HTTP. En el enfoque tradicional, escribirías:
let client = HttpClient::new( "https://api.example.com", Duration::from_secs(30), 5, true, None, vec!["Accept: application/json".to_string()], ...);Esto es ilegible y es fácil equivocarse en el orden de los argumentos. Builder resuelve este problema.
Implementación en Rust
En Rust, el patrón Builder se implementa mediante una estructura separada con métodos setters que devuelven self:
#[derive(Debug)]pub struct HttpClient { base_url: String, timeout: Duration, max_retries: u32, verify_ssl: bool, headers: Vec<String>,}
pub struct HttpClientBuilder { base_url: String, timeout: Duration, max_retries: u32, verify_ssl: bool, headers: Vec<String>,}
impl HttpClientBuilder { pub fn new(base_url: &str) -> Self { HttpClientBuilder { base_url: base_url.to_string(), timeout: Duration::from_secs(30), max_retries: 3, verify_ssl: true, headers: Vec::new(), } }
pub fn timeout(mut self, duration: Duration) -> Self { self.timeout = duration; self }
pub fn max_retries(mut self, n: u32) -> Self { self.max_retries = n; self }
pub fn no_ssl(mut self) -> Self { self.verify_ssl = false; self }
pub fn add_header(mut self, header: &str) -> Self { self.headers.push(header.to_string()); self }
pub fn build(self) -> Result<HttpClient, String> { if self.base_url.is_empty() { return Err("base_url cannot be empty".to_string()); } Ok(HttpClient { base_url: self.base_url,