3.6 Комнаты в гримуаре
В мастерской без комнат всё находится. Для этого нужно знать, где именно - знание приходит само, объяснять его заранее не принято. Пока один мастер и один верстак, этого достаточно. Потом верстаков становится больше. Знание остаётся прежним. Это принято называть опытом. Правильное слово - накопленным беспорядком.
Попытка
Восемь типов, шесть трейтов, двести пятьдесят строк в main.rs. Форма задачи, хранилище,
ошибки, голос - всё в одном файле. Разумное желание: дать каждой части свой файл.
Начнём с малого - перенесём только Status и Task.
Создадим src/task.rs с этими двумя типами:
// src/task.rs
#[derive(Debug)]
enum Status {
Todo,
Done,
}
#[derive(Debug)]
struct Task {
id: u64,
title: String,
status: Status,
}
Из main.rs эти два объявления удаляем. Компилятор замечает:
error[E0412]: cannot find type `Task` in this scope
--> src/main.rs:XX:XX
|
XX | impl Task {
| ^^^^ not found in this scope
Что замечает компилятор
Rust не включает файлы автоматически. Создать src/task.rs - не значит сообщить о нём
компилятору. Для этого нужно объявление в src/main.rs:
// src/main.rs
mod task;
mod task; говорит: найди src/task.rs и включи его содержимое как модуль с именем
task. Теперь модуль существует, но Task и Status в текущую область видимости ещё не
принесены. Добавим:
// src/main.rs
use task::{Status, Task};
Попробуем снова:
error[E0603]: struct `Task` is private
--> src/main.rs:XX:XX
|
XX | use task::{Status, Task};
| ^^^^^^ private struct
Компилятор видит модуль и знает, что Task там есть - но она закрыта. В Rust всё
приватно по умолчанию: определить тип в модуле не значит открыть его для использования
снаружи. Для этого служит 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 на имени типа делает его видимым снаружи. pub на поле открывает поле для чтения
и записи извне модуля - без него структуру видно, а до её содержимого не добраться.
Заклинание
Три файла. Каждый хранит то, что ему принадлежит. Начнём с src/error.rs - он ни от
чего внутри крейта не зависит:
// src/error.rs - NEW
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),
}
}
}
Затем src/task.rs. Он использует типы из error.rs - и обращается к ним через crate::,
путь от корня крейта:
// src/task.rs - NEW
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 - здесь, рядом с Task: пока он нужен только ей. src/store.rs обратится
к нему через crate::task::HasId.
Теперь src/store.rs. Поле items стало приватным - store.items[0] из main.rs
больше не достать. В этом и есть смысл модуля: детали устройства хранятся внутри.
Взамен прямого доступа - метод get_mut:
// src/store.rs - NEW
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 появился не по плану - его потребовала приватность поля. Это и есть
эффект модуля: граница видимости вынуждает думать о том, что должно быть снаружи.
Наконец, src/main.rs - объявления модулей, импорты, обновлённый вызов:
// src/main.rs - CHANGED
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(); // CHANGED: was &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(())
}
Вывод не изменился:
#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
Тесты следуют той же схеме - полная сюита в
3-a-voice/06-rooms-in-the-grimoire/.
make ci проходит. У каждого типа теперь своя комната.
Полный код
tqдля этой главы -3-a-voice/06-rooms-in-the-grimoire/.