← Оглавление

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/.