← Índice

3.4 Un almacén para todo

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

Un buen gremio no confía en la memoria del artesano. Confía en un documento: la pieza debe aceptar, encontrar, enumerar - el artesano firmó. El almacén puede hacer todo lo necesario. Capacidad sin obligación firmada suele llamarse fiabilidad. La palabra exacta es coincidencia.

Lo que necesitas

Acabamos de construir TaskStore, donde los métodos add, get y all viven en impl TaskStore. Pero en ningún lugar está escrito que el almacén deba tenerlos - elimina all y el compilador no dice nada.

Se necesita un contrato con nombre: una descripción de lo que el almacén está obligado a hacer. Así la implementación y el requisito viven por separado - y cualquier discrepancia entre ellos la detecta el compilador, no solo quien escribió el código.

La construcción

Para que un almacén pueda encontrar un elemento por identificador, cada elemento debe poder proporcionar uno. Este es el requisito mínimo para cualquier cosa que el almacén acepte:

trait HasId {
    fn id(&self) -> u64;
}

Para Task:

impl HasId for Task {
    fn id(&self) -> u64 {
        self.id
    }
}

Varios métodos devuelven Result con el mismo tipo de error - y esto se repite en las firmas. Un alias de tipo elimina la repetición:

type TqResult<T> = Result<T, TqError>;
Sin aliasCon alias
fn add(...) -> Result<(), TqError>fn add(...) -> TqResult<()>
fn get(...) -> Result<&T, TqError>fn get(...) -> TqResult<&T>
Task::new(...) -> Result<Task, TqError>Task::new(...) -> TqResult<Task>

T es el tipo de éxito: TqResult<()> significa “nada”, TqResult<&Task> significa “una referencia a una tarea”.

El contrato del almacén se describe mediante un trait. T es un marcador de posición para el tipo de elemento; : HasId dice que ese tipo sabe cómo proporcionar un identificador:

trait Store<T: HasId> {
    fn add(&mut self, item: impl Into<T>) -> TqResult<()>;
    fn get(&self, id: u64) -> TqResult<&T>;
    fn all(&self) -> &[T];
}

TaskStore implementa este contrato. Los métodos se mueven de impl TaskStore a impl Store<Task> for TaskStore - impl TaskStore conserva solo el constructor:

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

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

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

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

impl Store<Task> for TaskStore - “TaskStore cumple el contrato Store para tareas.” El compilador comprueba: los tres métodos están presentes, las firmas coinciden. En get se llama a task.id() - el método de HasId. En add se escribe directamente el campo task.id: HasId solo proporciona lectura.

El resultado

En main, solo cambia la firma - Result<(), TqError> se convierte en TqResult<()>. Todo lo demás queda intacto:

fn main() -> TqResult<()> {

Los tests existentes pasan sin cambios. Ese es su trabajo: confirmar que los cambios internos no han roto el comportamiento desde fuera.

make ci pasa. TaskStore ha firmado: impl Store<Task> for TaskStore - no son solo métodos, son una obligación.

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


Lore: Restricciones en un trait

Store<T: HasId> - la restricción se pone directamente en el parámetro del trait. No es posible implementar Store<T> para un tipo sin HasId; el compilador lo comprobará en cualquier intento.

Cuando hay varias restricciones, la notación T: HasId + Clone se alarga. En ese caso, usa where:

impl<T> Store<T> for TaskStore
where
    T: HasId,
{
    // ...
}

Ambas formas compilan de forma idéntica. where es más cómodo con restricciones más largas.