Patrones de diseño en Rust: guía práctica con ejemplos
Los patrones de diseño son soluciones probadas para problemas típicos de desarrollo. En Rust, gracias a su sistema único de ownership y modelo de memoria, la implementación de patrones clásicos a menudo difiere de los lenguajes con recolección de basura. En este artículo analizaremos tres patrones clave: Singleton, Builder y Strategy, adaptados a las particularidades de Rust.
1. Patrón Singleton en Rust: estado global sin unsafe
Singleton garantiza que una clase tiene una única instancia y proporciona un punto de acceso global a ella. En Rust, el crate once_cell o el estable std::sync::OnceLock (desde Rust 1.70) son ideales para esto.
Implementación con OnceLock
use std::sync::OnceLock;
pub struct Logger { level: String,}
impl Logger { pub fn global() -> &'static Logger { static INSTANCE: OnceLock<Logger> = OnceLock::new(); INSTANCE.get_or_init(|| Logger { level: "info".to_string(), }) }
pub fn log(&self, msg: &str) { println!("[{}] {}", self.level, msg); }}
fn main() { let logger = Logger::global(); logger.log("¡Hola, Rust!"); // Logger::global() devolverá la misma instancia}Ventajas: Seguridad (sin unsafe), inicialización perezosa, seguridad en concurrencia. Desventaja: el estado global dificulta las pruebas.
2. Patrón Builder: construcción de objetos complejos
Builder separa la construcción de un objeto complejo de su representación. En Rust es especialmente útil para estructuras con múltiples campos opcionales.
Ejemplo: configuración de un cliente HTTP
pub struct HttpClient { url: String, timeout: u64, headers: Vec<(String, String)>,}
pub struct HttpClientBuilder { url: String, timeout: Option<u64>, headers: Vec<(String, String)>,}
impl HttpClientBuilder { pub fn new(url: &str) -> Self { HttpClientBuilder { url: url.to_string(), timeout: None, headers: vec![], } }
pub fn timeout(mut self, secs: u64) -> Self { self.timeout = Some(secs); self }
pub fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
pub fn build(self) -> HttpClient { HttpClient { url: self.url, timeout: self.timeout.unwrap_or(30), headers: self.headers, } }}
fn main() { let client = HttpClientBuilder::new("https://api.example.com") .timeout(10) .header("Authorization", "Bearer token123") .build(); println!("URL del cliente: {}", client.url);}Importante: Usar self (no &self) en los métodos del builder permite evitar clonaciones y hace la API más ergonómica.
3. Patrón Strategy: algoritmos como estrategias intercambiables
Strategy define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables. En Rust esto se implementa mediante traits y despacho dinámico (Box<dyn Trait>).
Ejemplo: sistema de ordenamiento
trait SortStrategy { fn sort(&self, data: &mut [i32]);}
struct BubbleSort;impl SortStrategy for BubbleSort { fn sort(&self, data: &mut [i32]) { let n = data.len(); for i in 0..n { for j in 0..n - i - 1 { if data[j] > data[j + 1] { data.swap(j, j + 1); } } } }}
struct QuickSort;impl SortStrategy for QuickSort { fn sort(&self, da
```