3.6 Las salas del grimorio
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
En un taller sin salas, todo puede encontrarse. Para eso hay que saber exactamente dónde - ese conocimiento llega solo, explicarlo de antemano no es costumbre. Con un artesano y un banco, es suficiente. Luego hay más bancos. El conocimiento permanece igual. Esto suele llamarse experiencia. La palabra exacta es desorden acumulado.
El intento
Ocho tipos, seis traits, doscientas cincuenta líneas en main.rs. La forma de la tarea, el
almacén, los errores, la voz - todo en un archivo. Un deseo razonable: dar a cada parte su
propio archivo.
Empieza poco a poco - mueve solo Status y Task. Crea src/task.rs con esos dos tipos:
// src/task.rs
#[derive(Debug)]
enum Status {
Todo,
Done,
}
#[derive(Debug)]
struct Task {
id: u64,
title: String,
status: Status,
}
Elimina esas dos declaraciones de main.rs. El compilador lo nota:
error[E0412]: cannot find type `Task` in this scope
--> src/main.rs:XX:XX
|
XX | impl Task {
| ^^^^ not found in this scope
Lo que sabe el compilador
Rust no incluye archivos automáticamente. Crear src/task.rs no informa al compilador de
su existencia. Eso requiere una declaración en src/main.rs:
// src/main.rs
mod task;
mod task; dice: encuentra src/task.rs e incluye su contenido como un módulo llamado
task. El módulo ahora existe, pero Task y Status no han sido traídos al ámbito actual.
Añade:
// src/main.rs
use task::{Status, Task};
Inténtalo de nuevo:
error[E0603]: struct `Task` is private
--> src/main.rs:XX:XX
|
XX | use task::{Status, Task};
| ^^^^^^ private struct
El compilador ve el módulo y sabe que Task está dentro - pero está cerrado. En Rust todo
es privado por defecto: definir un tipo en un módulo no lo abre para su uso fuera. Para eso
está pub:
// src/task.rs
#[derive(Debug)]
pub enum Status {
Todo,
Done,
}
#[derive(Debug)]
pub struct Task {
pub id: u64,
pub title: String,
pub status: Status,
}
pub en el nombre del tipo lo hace visible desde fuera. pub en un campo abre ese campo
para lectura y escritura desde fuera del módulo - sin él el struct es visible pero su
contenido es inalcanzable.
El conjuro
Tres archivos. Cada uno contiene lo que le pertenece. Empieza con src/error.rs - no
depende de nada más dentro del crate:
// src/error.rs - NUEVO
use std::fmt;
#[derive(Debug)]
pub enum TqError {
EmptyTitle,
NotFound(u64),
}
pub type TqResult<T> = Result<T, TqError>;
impl fmt::Display for TqError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TqError::EmptyTitle => write!(f, "task title cannot be empty"),
TqError::NotFound(id) => write!(f, "task {} not found", id),
}
}
}
Luego src/task.rs. Usa tipos de error.rs - y se refiere a ellos a través de crate::,
la ruta desde la raíz del crate:
// src/task.rs - NUEVO
use crate::error::{TqError, TqResult};
use std::fmt;
pub trait HasId {
fn id(&self) -> u64;
}
#[derive(Debug)]
pub enum Status {
Todo,
Done,
}
#[derive(Debug)]
pub struct Task {
pub id: u64,
pub title: String,
pub status: Status,
}
impl Task {
pub fn new(id: u64, title: &str) -> TqResult<Task> {
if title.is_empty() {
return Err(TqError::EmptyTitle);
}
Ok(Task {
id,
title: String::from(title),
status: Status::Todo,
})
}
pub fn complete(&mut self) {
self.status = Status::Done;
}
pub fn is_done(&self) -> bool {
match self.status {
Status::Done => true,
Status::Todo => false,
}
}
}
impl From<&str> for Task {
fn from(title: &str) -> Self {
Task {
id: 0,
title: title.to_string(),
status: Status::Todo,
}
}
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}: {} [{:?}]", self.id, self.title, self.status)
}
}
impl HasId for Task {
fn id(&self) -> u64 {
self.id
}
}
HasId vive aquí, junto a Task: por ahora solo ella lo necesita. src/store.rs se
referirá a él a través de crate::task::HasId.
Ahora src/store.rs. El campo items se ha vuelto privado - store.items[0] de main.rs
ya no es accesible. Ese es el efecto del módulo: los detalles de implementación permanecen
dentro. En lugar del acceso directo - un método get_mut:
// src/store.rs - NUEVO
use crate::error::{TqError, TqResult};
use crate::task::{HasId, Task};
pub trait Store<T: HasId> {
fn add(&mut self, item: impl Into<T>) -> TqResult<()>;
fn get(&self, id: u64) -> TqResult<&T>;
fn get_mut(&mut self, id: u64) -> TqResult<&mut T>;
fn all(&self) -> &[T];
}
pub struct TaskStore {
items: Vec<Task>,
next_id: u64,
}
impl TaskStore {
pub 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)
.ok_or(TqError::NotFound(id))
}
fn get_mut(&mut self, id: u64) -> TqResult<&mut Task> {
self.items
.iter_mut()
.find(|task| task.id() == id)
.ok_or(TqError::NotFound(id))
}
fn all(&self) -> &[Task] {
&self.items
}
}
get_mut no estaba planeado - el campo privado lo exigió. Ese es el efecto de un límite de
módulo: la visibilidad te obliga a pensar en qué debe ser accesible desde fuera.
Finalmente, src/main.rs - declaraciones de módulos, importaciones, llamadas actualizadas:
// src/main.rs - CAMBIADO
mod error;
mod store;
mod task;
use error::TqResult;
use store::{Store, TaskStore};
use task::Task;
fn main() -> TqResult<()> {
let mut store = TaskStore::new();
store.add("Buy coffee")?;
store.add("Buy milk")?;
store.add("Buy eggs")?;
for task in store.all() {
println!("{}", task);
}
match store.get(1) {
Ok(task) => println!("found: {}", task),
Err(e) => println!("{}", e),
}
match store.get(99) {
Ok(task) => println!("{}", task),
Err(e) => println!("{}", e),
}
store.get_mut(1)?.complete(); // CAMBIADO: antes &mut store.items[0]
println!("done: {}", store.get(1)?.is_done());
match Task::new(0, "") {
Ok(task) => store.add(task)?,
Err(e) => println!("rejected: {}", e),
}
Ok(())
}
La salida no cambia:
#1: Buy coffee [Todo]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
found: #1: Buy coffee [Todo]
task 99 not found
done: true
rejected: task title cannot be empty
Los tests siguen el mismo esquema - la suite completa está en
3-a-voice/06-rooms-in-the-grimoire/.
make ci pasa. Cada tipo tiene ahora su propia sala.
El código completo de
tqpara este capítulo está en3-a-voice/06-rooms-in-the-grimoire/.