Default y el patrón constructor (builder)

Se puede implementar el rasgo Default para inicializar un struct o enum a los valores que se consideren más habituales. El patrón constructor (builder) se usa de forma combinada con esto para facilitar a los usuarios realizar aquellos cambios que quieran. En primer lugar se va a ver Default. Los tipos generales de Rust ya implementan Default. Son valores bastante habituales: 0, "" (cadenas de caracteres vacía), false, etc.

fn main() {
    let default_i8: i8 = Default::default();
    let default_str: String = Default::default();
    let default_bool: bool = Default::default();

    println!("'{}', '{}', '{}'", default_i8, default_str, default_bool);
}

Lo anterior imprime '0', ''. 'false'.

Así, Default es como la función new, salvo que no hay que darle nigún valor. En primer lugar, se va a mostrar un struct uqe no implemente Default. Tendrá una función new que se usa para crear un personaje denominado Billy con algunas características.

struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new(nombre: String, edad: u8, estatura: u32, peso: u32, vivo: bool) -> Self {
        Self {
            nombre,
            edad,
            estatura,
            peso,
            estadovital: if vivo { EstadoVital::Vivo } else { EstadoVital::Muerto },
        }
    }
}

fn main() {
    let personaje_1 = Personaje::new("Billy".to_string(), 15, 170, 70, true);
}

Puede que en este mundo, la mayoría de los personajes se llamen Billy, tengan edad de 15 años, estatura de 170, peso de 70 y estén vivos. Para ello, se puede implementar Default de forma que se pueda crear un personaje estándar con solo Personaje::default(). Se haría de la siguiente forma:

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new(nombre: String, edad: u8, estatura: u32, peso: u32, vivo: bool) -> Self {
        Self {
            nombre,
            edad,
            estatura,
            peso,
            estadovital: if vivo { EstadoVital::Vivo } else { EstadoVital::Muerto },
        }
    }
}

impl Default for Personaje {
    fn default() -> Self {
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }
}

fn main() {
    let personaje_1 = Personaje::default();

    println!(
        "El personaje {:?} tiene {:?} años.",
        personaje_1.nombre, personaje_1.edad
    );
}

El código anterior imprime El personaje "Billy" tiene 15 años..

El patrón constructor (builder)

A continuación se mostrará cómo utilizar el patrón constructor. Se mantiene el valor por defecto usando el rasgo Default. A veces, algunos personajes solo serán ligeramente diferentes a "Billy". El patrón constructor establece una cadena de métodos para cambiar un valor cada vez. A continuación se muestra uno de estos métodos para el Personaje:

#![allow(unused)]
fn main() {
fn estatura(mut self, estatura: u32) -> Self {    // 🚧
    self.estatura = estatura;
    self
}
}

Se debe prestar atención a que toma como parámetro un mut self. Esto ya se vio anteriormente. No es una referencia &mut self). Toma propiedad de Self y con mut es modificable, aunque no lo fuese antes. Esto es debido a que .estatura() obtiene la completa propiedad y nadie lo puede tocar, por lo que es seguro usar mut y el compilador no se queja. Luego solo le cambia el valor self.estatura y devuelve Self (que es de tipo Personaje).

En el ejemplo, se crean tres métodos constructores. Son muy parecidos:

#![allow(unused)]
fn main() {
fn estatura(mut self, estatura: u32) -> Self {     // 🚧
    self.estatura = estatura;
    self
}

fn peso(mut self, peso: u32) -> Self {
    self.peso = peso;
    self
}

fn nombre(mut self, nombre: &str) -> Self {
    self.nombre = nombre.to_string();
    self
}
}

Cada uno de estos métodos modifica una variable y devuelve Self: esto es lo que define a un patrón constructor. Así, se puede escribir lo siguiente para construir un personaje: let personaje_1 = Personaje::default().estatura(180).peso(60).nombre("Bobby");- Si se está construyendo una librería, esto hace que sea más fácil el crear nuevos elementos ya que queda legible lo que se está haciendo. Con estos cambios, el código queda así:

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new(nombre: String, edad: u8, estatura: u32, peso: u32, vivo: bool) -> Self {
        Self {
            nombre,
            edad,
            estatura,
            peso,
            estadovital: if vivo { EstadoVital::Vivo } else { EstadoVital::Muerto },
        }
    }
    fn estatura(mut self, estatura: u32) -> Self {     
        self.estatura = estatura;
        self
    }

    fn peso(mut self, peso: u32) -> Self {
        self.peso = peso;
        self
    }

    fn nombre(mut self, nombre: &str) -> Self {
        self.nombre = nombre.to_string();
        self
    }
}

impl Default for Personaje {
    fn default() -> Self {
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }
}

fn main() {
    let personaje_1 = Personaje::default().estatura(180).peso(60).nombre("Bobby");;

    println!("{:?}", personaje_1);
}

Un último método que se suele añadir es .build(). Este método es una especie de validación final. Por ejemplo, para comprobar que la estatura no contenga un valor de 5000. Con este método build() que debería devolver un Result, se pueden establecer validaciones para comprobar si es correcto el elemento que se está construyendo. Si lo es, se puede devolver Ok(Self).

En primer lugar, se modifica el método new() para que no reciba parámetros. Se evita que los usuarios sean libres de crear cualquier persona con cualquier valor. Los valores que utiliza la implementación de Default se pasan a .new(), que queda así:

#![allow(unused)]
fn main() {
    fn new() -> Self {    // 🚧
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }
}

Así, deja de ser necesaria la implementación del rasgo Default. Por tanto, se puede eliminar del código, que queda así:

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new() -> Self {    
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
        }
    }

    fn estatura(mut self, estatura: u32) -> Self {     
        self.estatura = estatura;
        self
    }

    fn peso(mut self, peso: u32) -> Self {
        self.peso = peso;
        self
    }

    fn nombre(mut self, nombre: &str) -> Self {
        self.nombre = nombre.to_string();
        self
    }
}

fn main() {
    let personaje_1 = Personaje::new().estatura(180).peso(60).nombre("Bobby");;

    println!("{:?}", personaje_1);
}

Lo anterior, imprime lo mismo que antes: Personaje { nombre: "Bobby", edad: 15, estatura: 180, peso: 60, estadovital: Vivo }.

Con el código anterior, casi se puede implementar el método build(), pero hay un problema: ¿cómo se fuerza al usuario a que lo utilice después de poner todos los valores de los diferentes atributos? En este momento, el usuario puede escribir solo let x = Personaje::new().estatura(76767); y obtener un Personaje con una estatura imposible. Hay diversas formas de hacerlo. Una de ellas consiste en añadir a Personaje un atributo se_puede_usar: bool.

#![allow(unused)]
fn main() {
    fn new() -> Self {    // 🚧
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
            se_puede_usar: true, // .new() devuelve un elemento válido, se puede usar.
        }
    }
}

Los demás métodos constructores como .estatura(), establecerán se_puede_usar a false. Solo .build() lo volverá a establecer a true. Así, el usuario se verá obligado a llamar a este método de comprobación. Se validará que la estatura no es superior a 250 y el peso no es superior a 300. También se comprobará que los personajes no puedan llamarse smurf.

El método .build() queda como sigue:

#![allow(unused)]
fn main() {
fn build(mut self) -> Result<Personaje, String> {      // 🚧
    if self.estatura < 250 && self.peso < 300 && !self.nombre.to_lowercase().contains("smurf") {
        self.se_puede_usar = true;
        Ok(self)
    } else {
        Err("No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)"
            .to_string())
    }
}
}

!self.nombre.to_lowercase().contains("smurf") sirve para asegurar que el usuario no escribe variaciones de esta palabra como "SMURF" o "SoySmurf". Pasa la cadena de caracteres a minúscula y comprueba que no contenga el texto. La ! significa no.

Si el personaje es correcto, se activa se_puede_usar a true y se devuelve el personaje dentro del elemento Ok.

Así queda el código completo. Se muestra creando tres personajes que no cumplen los requisitos y un personaje que sí.

#[derive(Debug)]
struct Personaje {
    nombre: String,
    edad: u8,
    estatura: u32,
    peso: u32,
    estadovital: EstadoVital,
    se_puede_usar: bool,
}

#[derive(Debug)]
enum EstadoVital {
    Vivo,
    Muerto,
    NuncaVivo,
    Desconocido
}

impl Personaje {
    fn new() -> Self {    
        Self {
            nombre: "Billy".to_string(),
            edad: 15,
            estatura: 170,
            peso: 70,
            estadovital: EstadoVital::Vivo,
            se_puede_usar: true,
        }
    }

    fn estatura(mut self, estatura: u32) -> Self {     
        self.estatura = estatura;
        self.se_puede_usar = false;
        self
    }

    fn peso(mut self, peso: u32) -> Self {
        self.peso = peso;
        self.se_puede_usar = false;
        self
    }

    fn nombre(mut self, nombre: &str) -> Self {
        self.nombre = nombre.to_string();
        self.se_puede_usar = false;
        self
    }

    fn build(mut self) -> Result<Personaje, String> { 
        if self.estatura < 250 && self.peso < 300 && !self.nombre.to_lowercase().contains("smurf") {
            self.se_puede_usar = true;
            Ok(self)
        } else {
            Err("No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)"
                .to_string())
        }
    }
}

fn main() {
 let personaje_con_smurf = Personaje::new().nombre("jeje, soy Smurf!!").build(); // Este contiene "smurf" - no es correcto
    let personaje_demasiado_alto = Personaje::new().estatura(400).build(); // Demasiado alto - no es correcto
    let personaje_demasiado_pesado = Personaje::new().peso(500).build(); // Demasiado pesado - no es correcto
    let personaje_correcto = Personaje::new()
        .nombre("Billybrobby")
        .estatura(180)
        .peso(100)
        .build();   // Este es correcto, peso, estatura y nombre

    // Las cuatro variables no son Personaje, son Result<Personaje, String>. Se meten en un vector y se ve el resultado:
    let personaje_vec = vec![personaje_con_smurf, personaje_demasiado_alto, personaje_demasiado_pesado, personaje_correcto];

    for Personaje in personaje_vec { // Se imprimirá para ver si hay un error o no
        match Personaje {
            Ok(personaje_info) => println!("{:?}", personaje_info),
            Err(err_info) => println!("{}", err_info),
        }
        println!(); // Añade una línea separadora
    }
}

El código anterior da como resultado:

No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)

No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)

No se puede crear el Personaje. Los Personajes deben tener:
1) estatura menor a 250
2) peso menor a 300
3) nombre diferente a Smurf (es una palabra fea)

Personaje { nombre: "Billybrobby", edad: 15, estatura: 180, peso: 100, estadovital: Vivo, se_puede_usar: true }