El desarrollo idiomático en Rust busca trasladar errores de tiempo de ejecución hacia el tiempo de compilación. Para lograr esto, se utilizan patrones que aprovechan el sistema de tipos y las reglas de propiedad (ownership).
El Patrón Builder se utiliza cuando un objeto requiere una configuración compleja con múltiples parámetros opcionales. En lugar de tener constructores con múltiples argumentos (que son difíciles de leer y propensos a errores), se usa una estructura intermedia que acumula los datos. Si se implementa incorrectamente (por ejemplo, permitiendo la creación de un objeto sin datos esenciales), se pierde la garantía de integridad. Un Builder robusto utiliza un método build() que devuelve un Result para validar la configuración antes de instanciar el objeto final.
El Patrón Typestate eleva la seguridad de una máquina de estados al nivel de tipos. En lugar de usar un campo is_connected: bool que puede ser modificado erróneamente, se utilizan tipos diferentes para representar cada estado (ej. Conectado vs Desconectado). Esto permite que ciertos métodos solo existan para ciertos estados; si intentas ejecutar una consulta en una conexión que no ha sido establecida, el compilador lo detectará como un error de tipo, impidiendo la ejecución de código lógico inválido.
El patrón RAII (Resource Acquisition Is Initialization) se implementa mediante el trait Drop. En Rust, esto garantiza que los recursos (como conexiones de red o archivos) se liberen automáticamente en cuanto la variable sale de su alcance (scope), evitando fugas de memoria o de descriptores de archivos. Si no se utiliza, el programador debe liberar el recurso manualmente, lo que en lenguajes con gestión manual de memoria es una fuente constante de errores.
use std::marker::PhantomData;
// --- PATRÓN TYPESTATE: Marcadores de estado ---
struct Desconectado;
struct Conectado;
// --- RAII Y TYPESTATE: El objeto principal ---
struct Conexion<S> {
host: String,
// PhantomData es necesario para usar el tipo genérico S sin que sea un campo real
_estado: PhantomData<S>,
}
impl Conexion<Desconectado> {
// Transición de estado: Consume 'Desconectado' y retorna 'Conectado'
fn conectar(self) -> Conexion<Conectado> {
println!("Conectando a {}...", self.host);
Conexion {
host: self.host,
_estado: PhantomData,
}
}
}
impl Conexion<Conectado> {
// Este método solo existe para la instancia de tipo Conexion<Conectado>
fn ejecutar_query(&self, sql: &str) {
println!("Ejecutando en {}: {}", self.host, sql);
}
}
// --- RAII: Implementación de Drop para limpieza automática ---
impl Drop for Conexion<Conectado> {
fn drop(&mut self) {
println!("Liberando recursos de la conexión con {}...", self.host);
}
}
// --- PATRÓN BUILDER: Construcción de la configuración ---
struct ConexionBuilder {
host: Option<String>,
puerto: u16,
}
impl ConexionBuilder {
fn nuevo() -> Self {
Self {
host: None,
puerto: 8080,
}
}
// Método encadenable que consume 'self' para permitir fluidez
fn con_host(mut self, host: &str) -> Self {
self.host = Some(host.to_string());
self
}
fn con_puerto(mut self, puerto: u16) -> Self {
self.puerto = puerto;
self
}
// Validación final mediante Result
fn construir(self) -> Result<Conexion<Desconectado>, String> {
let host = self.host.ok_or("Error: La host es obligatoria")?;
Ok(Conexion {
host,
_estado: PhantomData,
})
}
}
fn main() -> Result<(), String> {
// 1. Uso del Builder para configurar el objeto
let config = ConexionBuilder::nuevo()
.con_host("127.0.0.1")
.con_puerto(5432)
.construir()?;
// 2. El Typestate impide ejecutar query en el estado 'Desconectado'
// config.ejecutar_query("SELECT 1"); // <--- ERROR DE COMPILACIÓN
// 3. Transición de estado segura
let conexion_activa = config.conectar();
// 4. Uso de la conexión solo cuando es válida
conexion_activa.ejecutar_query("SELECT * FROM usuarios;");
// 5. RAII: Al finalizar main, 'conexion_activa' sale de scope y se llama a drop()
Ok(())
}
Explicación del Código
El proceso comienza con ConexionBuilder::nuevo(), que inicializa la estructura con valores por defecto. El método con_host recibe un &str y lo transforma en un Option<String>, permitiendo encadenamiento mediante el retorno de self. Al llamar a construir(), se valida si host tiene un valor; si no es así, se retorna un Err con un mensaje, de lo contrario, se retorna una Conexion<Desconectado>.
La estructura Conexion<S> utiliza PhantomData<S> para informar al compilador que, aunque no almacena el tipo S directamente, debe tratarla como parte de su identidad de tipo. Esto permite implementar bloques impl específicos para Conexion<Desconectado> y Conexion<Conectado>. El método conectar(self) es crucial: consume la instancia original (moviendo el valor) y devuelve una nueva Conexion<Conectado>, garantizando que la versión desconectada ya no sea accesible.
El método ejecutar_query solo está definido para la implementación de Conexion<Conectado>, lo que asegura que es imposible llamar a una consulta en una conexión no establecida. Finalmente, el trait Drop implementado para Conexion<Conectado> asegura que, cuando conexion_activa sale de su ámbito en main, se ejecute el mensaje de liberación de recursos, implementando el patrón RAII de forma automática.
N° 128