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 alias | Con 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
tqpara este capítulo está en3-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.