Introducción

Participación

Si estás interesado en contribuir a este libro, revisa las recomendaciones de contribución.

Patrones de diseño

En el desarrollo de software es habitual que nos encontremos con problemas que comparten similitudes independientemente del entorno en el que aparecen. Aunque los detallas de implementación son determinantes para resolver la tarea que tengamos entre manos, podemos abstraernos de estas particularidades para encontrar aquellas prácticas habituales que se pueden aplicar de manera general.

Los patrones de diseño son colecciones de soluciones, probadas y reutilizables, a problemas recurrentes de ingeniería. Permiten que nuestro software sea modular, mantenible y extensible. Más aún, estos patrones proporcionan un lenguaje común a los desarrolladores, lo que los convierte en una herramienta excelente para que la comunicación de los equipos de trabajo sea efectiva durante la resolución de cualquier problema.

Los patrones de diseño en Rust

Rust no es un lenguaje orientado a objetos. Tiene un conjunto de características que lo hacen único:

  • Elementos funcionales.
  • Sistema de tipos fuerte.
  • Revisor de préstamos.

Por este motivo, los patrones de diseño de Rust son diferentes a los de otros lenguajes de programación tradicionales orientados a objetos. Por eso decidimos escribir este libro. ¡Esperamos que disfrutes de su lectura!

El libro está dividido en tres capítulos principales:

  • Idiomatismos (idioms): recomendaciones a seguir al codificar. Son normas sociales de la comunidad. Solo se deberían romper cuando se tenga una buena razón para ello.
  • Patrones de diseño: métodos para resolver problemas habituales de codificación.
  • Antipatrones: métodos para resolver problemas habituales de codificación. Sin embargo, mientras los patrones de diseño aportan beneficios al código, los antipatrones crean más problemas que los que intentan resolver.

Traducciones

Si quieres añadir una traducción, por favor, abre un tema (issue) en el repositorio principal

Contribuciones

#TODO - Pendiente de traducir

Idiomatismos (idioms)

Los idiomatismos son estilos, recomendaciones y patrones que se usan de común acuerdo por una comunidad. La escritura de código idiomático permite que otros desarrolladores comprendan mejor de qué va el código.

Después de todo, al ordenador solo se importa el código máquina que genera el compilador. Por el contrario, el código fuente beneficia principalmente al desarrollador. Por eso, ya que tenemos este nivel de abstracción ¿Por qué no hacerlo más legible?

Recuerda el principio KISS: "Keep It Simple, Stupid"1. Aboga por que "la mayoría de los sistemas funcionan mejor si se mantienen simples en lugar de complejos; por lo tanto, la simplicidad debería ser un objetivo fundamental en el diseño y la complejidad innecesaria debería evitarse".

El código está ahí para que lo entiendan los humanos, no los ordenadores.

1

N.T.: mantenlo simple, estúpido

El uso de tipos prestados como argumentos

Descripción

Cuando necesitamos decidir el tipo del argumento de una función, el uso de un tipo que implemente Deref (que permite el uso implícito de la conversión por derreferenciación) aumenta la flexibilidad del código. Gracias a esto, la función aceptará un mayor número de tipos de entrada.

Esto no se limita a los tipos punteros gruesos (fat pointers) o fragmentables (slice-able). De hecho, siempre se debería preferir el uso del tipo prestado (borrowed type) sobre tomar prestado el tipo propio (owned type). Por ejemplo, se prefiere &str sobre &String, &[T] sobre &Vec<T> o &T sobre &Box<T>.

Mediante el uso de tipos prestados se evitan capas de indirección para aquellas instancias en las que el tipo propio ya proporciona dicha indirección. Por ejemplo, un String ya dispone de una capa de indirección, por lo que &String se compone de dos capas de indirección. En lugar de esto, podemos evitarlo mediante el uso de &str como tipo del argumento de la función y dejar que &String se convierta a &str cuando se llame a la función.

Ejemplo

En el siguiente ejemplo se ilustrarán algunas diferencias entre el uso de &String como tipo para argumento de una función y el uso de &str en su lugar. En cualquier caso, estas ideas se aplican también al uso de &Vec<T> en lugar de &[T], o al uso de &Box<T> en lugar de &T.

Consideremos que deseamos determinar si una palabra contiene tres vocales consecutivas. No necesitamos la propiedad de la cadena de caracteres para determinar esto, por lo que solamente es necesaria una referencia.

El código podría ser como sigue:

fn three_vowels(word: &String) -> bool {
    let mut vowel_count = 0;
    for c in word.chars() {
        match c {
            'a' | 'e' | 'i' | 'o' | 'u' => {
                vowel_count += 1;
                if vowel_count >= 3 {
                    return true
                }
            }
            _ => vowel_count = 0
        }
    }
    false
}

fn main() {
    let ferris = "Ferris".to_string();
    let curious = "Curious".to_string();
    println!("{}: {}", ferris, three_vowels(&ferris));
    println!("{}: {}", curious, three_vowels(&curious));

    // Esto funciona bien, pero las siguientes dos líneas no compilarían:
    // println!("Ferris: {}", three_vowels("Ferris"));
    // println!("Curious: {}", three_vowels("Curious"));

}

El código anterior funciona bien porque pasamos un &String como parámetro. Si se quitan los comentarios de las últimas dos líneas, el ejemplo no compila. Esto se debe a que &str no puede convertirse de forma automática al tipo &String. Lo podemos resolver con solo cambiar el tipo del argumento de la función.

Si la declaración de la función se modifica a:

#![allow(unused)]
fn main() {
fn three_vowels(word: &str) -> bool {
}

Ambas líneas de código compilarán sin problemas e imprimirán la misma salida:

#![allow(unused)]
fn main() {
Ferris: false
Curious: true
}

Pero eso no es todo. Hay más que contar. Es posible que te hayas dicho a ti mismo: esto no me importa, nunca voy a usar un &'static str como parámetro de nada (como hemos hecho cuando usamos "Ferris" en la penúltima línea). Incluso, si ignoráramos este ejemplo concreto, puedes encontrar casos en los que el uso como argumento de &str te da más flexibilidad que el de &String.

Vamos a ver otro ejemplo en el que alguien nos pasa una frase y queremos determinar si alguna de las palabras de la frase contiene tres vocales consecutivas. Deberíamos usar la función que ya hemos definido y simplemente alimentarla de cada una de las palabras de la frase a analizar.

El código podría ser como el que sigue:

fn three_vowels(word: &str) -> bool {
    let mut vowel_count = 0;
    for c in word.chars() {
        match c {
            'a' | 'e' | 'i' | 'o' | 'u' => {
                vowel_count += 1;
                if vowel_count >= 3 {
                    return true
                }
            }
            _ => vowel_count = 0
        }
    }
    false
}

fn main() {
    let sentence_string =
        "Once upon a time, there was a friendly curious crab named Ferris".to_string();
    for word in sentence_string.split(' ') {
        if three_vowels(word) {
            println!("{} has three consecutive vowels!", word);
        }
    }
}

Al ejecutar este ejemplo con nuestra función con un argumento de tipo &str se imprime:

curious has three consecutive vowels!

Sin embargo, si se cambia la función para que su argumento sea de tipo &String, este ejemplo no funcionará. Esto se debe a que los fragmentos de una cadena de caracteres son de tipo &str y no &String: esta conversión requeriría una asignación de memoria costosa que no se hace implícita), mientras que la conversión de &String a &str es muy económica e implícita.

Vea también

Concatenar String con format!

Descripción

Es posible construir cadenas utilizando los métodos push y push_str sobre una variable de tipo mut String (de tipo cadena de caracteres modificable). También se puede usar su operador +. Sin embargo, puede ser más conveniente usar la macro format!, especialmente cuando se trata de crear la cadena de caracteres con una mezcla de cadenas de caracteres literales con otras no literales.

Ejemplo

#![allow(unused)]
fn main() {
fn say_hello(name: &str) -> String {
    // Podríamos construir la cadena de caracteres
    // resultante de forma manual 
    // let mut result = "Hello ".to_owned();
    // result.push_str(name);
    // result.push('!');
    // result

    // Pero el uso de format! es mejor.
    format!("Hello {}!", name)
}
}

Ventajas

El uso de format! suele ser la forma más sucinta y legible de combinar cadenas de caracteres.

Desventajas

No es la forma más eficiente de combinar cadenas de caracteres. La forma más eficiente de combinar cadenas es usar una mut String sobre la que se van realizando operaciones push (especialmente, si se ha prerreservado el espacio necesario para la cedena de caracteres).

Constructores

Descripción

Rust no dispone de constructores como elemento del lenguaje. A falta de ellos, para crear un objeto se usa por convención la función asociada denominada new.

#![allow(unused)]
fn main() {
/// Tiempo en segundos.
///
/// # Ejemplo
///
/// ```
/// let s = Second::new(42);
/// assert_eq!(42, s.value());
/// ```
pub struct Second {
    value: u64
}

impl Second {
    // Construye una nueva instancia de [`Second`].
    // Hay que observar que  es una función asociada:
    // es decir, no tiene self.
    pub fn new(value: u64) -> Self {
        Self { value }
    }

    /// Devuelve el valor en segundos.
    pub fn value(&self) -> u64 {
        self.value
    }
}
}

Constructores por defecto

Rust facilita un constructor por defecto mediante el uso del rasgo Default:

#![allow(unused)]
fn main() {
/// Tiempo en segundos.
///
/// # Ejemplo
///
/// ```
/// let s = Second::default();
/// assert_eq!(0, s.value());
/// ```
pub struct Second {
    value: u64
}

impl Second {
    /// Devuelve el tiempo en segundos.
    pub fn value(&self) -> u64 {
        self.value
    }
}

impl Default for Second {
    fn default() -> Self {
        Self { value: 0 }
    }
}
}

Default puede derivarse si todos los tipos de los campos de la estructura implementan Default, como sucede con el tipo Second. Así, es posible hacer lo siguiente:

#![allow(unused)]
fn main() {
/// Tiempo en segundos.
///
/// # Ejemplo
///
/// ```
/// let s = Second::default();
/// assert_eq!(0, s.value());
/// ```
#[derive(Default)]
pub struct Second {
    value: u64
}

impl Second {
    /// Devuelve el valor en segundos.
    pub fn value(&self) -> u64 {
        self.value
    }
}
}

Nota: es habitual y se espera que los tipos que se definan implementen ambos: Default y un constructor new vacío. Como new es, por convención, el constructor por defecto y los usuarios esperan que exista, es razonable que no tenga argumentos en su definición. Lo normal sería que su comportamiento funcioanl coincidera con los valores asignados con Default.

Pista: la ventaja de implementar o derivar Default es que el tipo que lo hace puede utilizarse allí donde se requiera el rasgo Default, por ejemplo, en cualquiera de las funciones *or_default de la librería estándar.

Vea también

El rasgo Default

Las colecciones son punteros inteligentes

Finalización en destructores

mem::{take(), replace()}

On-Stack Dynamic Dispatch

Interfaz de funciones extranjeras (Foreign function interface - FFI)

idiomatismos sobre errores (Errores idiomáticos)

Aceptar cadenas de caracteres

Pasar cadenas de caracteres

Iterar sobre un Option

Pasar variables a un Closure

Privacidad para ser extensible

Inicialización fácil de documentación

Mutabilidad temporal

Patrones de disñeo

De comportamiento (behavioural)

Comando

Intérprete

Nuevo tipo (Newtype)

Guardas RAII

Estrategia

Visitante

Creacionales

Constructor (builder)

Plegado (Fold)

Estructurales

Composición de estructuras

Preferi Crates (librerías) pequeñas

Contener código inseguro en módulos pequeños

Interfaz de funciones extranjeras (Foreign function interface - FFI)

APIs basadas en objetos

Consolidación de tipos en envoltoríos (Wrappers)

Antipatrones

Clonar para satisfacer el validador de préstamo (borrow checker)

#[deny(warnings)]

Polimorfismo con Deref

Programación funcional

Paradigmas de programación

Genéricos como clases de tipo

Recursos adicionales

Principios de diseño