Patrones de diseño en Rust: nivel avanzado con ejemplos
\n\nLos patrones de diseño son soluciones probadas para problemas típicos que ayudan a crear una arquitectura flexible, mantenible y escalable. En Rust, con su sistema de propiedad, préstamo y traits, muchos patrones clásicos adquieren una implementación única. En este artículo analizaremos patrones de diseño avanzados que son especialmente efectivos en el ecosistema de Rust.
\n\n1. Builder avanzado (Constructor)
\n\nEl patrón Builder clásico en Rust a menudo se implementa mediante una cadena de métodos que devuelven &mut Self. Sin embargo, a nivel avanzado utilizamos un Builder tipificado con tipos de estado para garantizar la corrección en tiempo de compilación.
Problema: control de campos obligatorios
\nUn Builder normal no garantiza que el usuario haya completado todos los campos obligatorios. Podemos olvidar llamar a .build() o no establecer un parámetro críticamente importante.
Solución: Builder con tipos fantasma
\nuse std::marker::PhantomData;\n\n// Tipos de estado para los campos\nstruct UsernameNotSet;\nstruct UsernameSet;\nstruct PasswordNotSet;\nstruct PasswordSet;\n\nstruct UserBuilder<U, P> {\n username: Option<String>,\n password: Option<String>,\n _username: PhantomData<U>,\n _password: PhantomData<P>,\n}\n\nimpl UserBuilder<UsernameNotSet, PasswordNotSet> {\n pub fn new() -> Self {\n Self {\n username: None,\n password: None,\n _username: PhantomData,\n _password: PhantomData,\n }\n }\n}\n\nimpl<P> UserBuilder<UsernameNotSet, P> {\n pub fn username(mut self, name: &str) -> UserBuilder<UsernameSet, P> {\n self.username = Some(name.to_string());\n UserBuilder {\n username: self.username,\n password: self.password,\n _username: PhantomData,\n _password: PhantomData,\n }\n }\n}\n\nimpl<U> UserBuilder<U, PasswordNotSet> {\n pub fn password(mut self, pwd: &str) -> UserBuilder<U, PasswordSet> {\n self.password = Some(pwd.to_string());\n UserBuilder {\n username: self.username,\n password: self.password,\n _username: PhantomData,\n _password: PhantomData,\n }\n }\n}\n\nimpl UserBuilder<UsernameSet, PasswordSet> {\n pub fn build(self) -> User {\n User {\n username: self.username.expect("Username must be set"),\n password: self.password.expect("Password must be set"),\n }\n }\n}\n\npub struct User {\n pub username: String,\n pub password: String,\n}\n\nfn main() {\n // Ahora el compilador no permitirá llamar a build() sin establecer ambos campos\n let user = UserBuilder::new()\n .username("alice")\n .password("secret123")\n .build();\n println!("User: {}", user.username);\n}\n\nVentaja: el error "campo olvidado" se detecta en tiempo de compilación, no en tiempo de ejecución.
\n\n2. Patrón State (Estado) mediante traits
\n\nEl patrón State permite que un objeto cambie su comportamiento al cambiar su estado interno. En Rust esto se implementa elegantemente mediante enumeraciones (enum) y traits.
\n\nImplementación con enum
\nenum ConnectionState {\n Disconnected,\n Connecting,\n Connected,\n}\n\nstruct Connection {\n state: ConnectionState,\n}\n\nimpl Connection {\n pub fn new() -> Self {\n Self { state: ConnectionState::Disconnected }\n }\n\n pub fn connect(&mut self) {\n match self.state {\n ConnectionState::Disconnected => {\n println!("Iniciando conexión...");\n self.state = ConnectionState::Connecting;\n }\n ConnectionState::Connecting => {\n println!("Ya conectando...");\n }\n ConnectionState::Connected => {\n println!("Ya conectado.");\n }\n }\n }\n\n pub fn disconnect(&mut self) {\n match self.state {\n ConnectionState::Connected => {\n println!("Desconectando...");\n self.state = ConnectionState::Disconnected;\n }\n _ => {\n println!("No se puede desconectar en este estado.");\n }\n }\n }\n}\n\nfn main() {\n let mut conn = Connection::new();\n conn.connect();\n conn.connect();\n conn.disconnect();\n}\n\nVentaja: el comportamiento depende del estado actual, y los estados inválidos son imposibles de representar.