← Índice

3.3 Un lugar propio

Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.

Una caja no se construye para durar eternamente. Se construye porque las cosas dispersas son incómodas - y una caja bien hecha da forma incluso cuando eventualmente se reemplaza. Esto suele llamarse solución temporal. La palabra exacta es primer paso.

Lo que necesitas

El enfoque actual tiene dos problemas visibles.

Se supone que el identificador es único, pero main decide cuál asignar. Con un puñado de tareas eso es fácil de seguir a mano. Un número omitido o repetido es algo que el compilador no notará; se necesita una única autoridad para emitir identificadores.

Tres funciones - add, get y list - siempre viajan juntas. Cada una toma &mut Vec<Task> o &[Task] como primer argumento. La lista de tareas es su estado compartido; vive en main y se pasa a mano en cada llamada.

Esto funciona mientras hay tres funciones y un artesano.

La construcción

Mueve el estado hacia dentro: que un struct posea la lista y se responsabilice de ella.

struct TaskStore {
    items: Vec<Task>,
    next_id: u64,
}

Un constructor y tres métodos - en un impl TaskStore:

impl TaskStore {
    fn new() -> Self {
        TaskStore {
            items: Vec::new(),
            next_id: 1,
        }
    }

    fn add(&mut self, task: impl Into<Task>) -> Result<(), TqError> {
        let mut task = task.into();
        task.id = self.next_id;
        self.next_id += 1;
        self.items.push(task);
        Ok(())
    }

    fn get(&self, id: u64) -> Result<&Task, TqError> {
        self.items
            .iter()
            .find(|task| task.id == id)
            .ok_or(TqError::NotFound(id))
    }

    fn all(&self) -> &[Task] {
        &self.items
    }
}

new crea un almacén vacío con next_id: 1. add acepta impl Into<Task> (capítulo 3.2): a través de From<&str> for Task del capítulo 3.1, un &str cumple los requisitos; el identificador se asigna dentro. get encuentra una tarea por id y devuelve una referencia - o TqError::NotFound si no hay ninguna. all devuelve un slice &[Task] - en lugar de list, que imprimía inmediatamente; ahora el llamador decide qué hacer con la colección.

El resultado

Las funciones libres add, get y list han desaparecido. main trabaja con TaskStore:

fn main() -> Result<(), TqError> {
    let mut store = TaskStore::new();

    store.add("Buy coffee")?;
    store.add("Buy milk")?;
    store.add("Buy eggs")?;

    for task in store.all() {
        println!("#{}: {} - {:?}", task.id, task.title, task.status);
    }

    match store.get(1) {
        Ok(task) => println!("found: #{}: {}", task.id, task.title),
        Err(e) => println!("error: {:?}", e),
    }

    match store.get(99) {
        Ok(task) => println!("#{}: {}", task.id, task.title),
        Err(TqError::NotFound(id)) => println!("task {} not found", id),
        Err(e) => println!("error: {:?}", e),
    }

    let task = &mut store.items[0];
    task.complete();
    println!("done: {}", task.is_done());

    match Task::new(0, "") {
        Ok(task) => store.add(task)?,
        Err(e) => println!("rejected: {:?}", e),
    }

    Ok(())
}

store.add("Buy coffee")? - un &str pasa directamente: From<&str> e impl Into<Task> se encuentran aquí. Completar una tarea funciona igual que antes, solo que tasks[0] se ha convertido en store.items[0].

Dos tests cubren el nuevo comportamiento:

#[test]
fn store_assigns_sequential_ids() {
    let mut store = TaskStore::new();
    store.add("A").unwrap();
    store.add("B").unwrap();
    assert_eq!(store.all()[0].id, 1);
    assert_eq!(store.all()[1].id, 2);
}

#[test]
fn store_get_returns_not_found() {
    let store = TaskStore::new();
    assert!(matches!(store.get(99), Err(TqError::NotFound(99))));
}

make ci pasa. Las tareas viven en TaskStore - añadir, buscar, enumerar. El almacén lo sabe todo sobre las tareas; sobre la posibilidad de que las tareas no sean el único tipo de dato que contiene, no sabe nada todavía.

El código completo de tq para este capítulo está en 3-a-voice/03-a-store-for-anything/.