← Оглавление

3.3 Свой ящик

Ящик строят не потому, что навсегда. Его строят потому, что рассыпанное неудобно - а правильный ящик задаёт форму даже тогда, когда сам в итоге меняется. Это принято называть временным решением. Правильное слово - первый шаг.

Что нужно

В текущем решении у нас две явные проблемы:

Идентификатор - что-то уникальное, однако main сам решает, какой назначить. Пока задач пара штук - легко отследить вручную. Пропущенный или повторный номер компилятор не заметит; нужен единый центр для выдачи номеров.

Три функции: add, get и list - которые всегда ходят вместе. У каждой первый аргумент

Это работает, пока функций три и мастер один.

Сборка

Перенесём состояние внутрь: пусть структура владеет списком и отвечает за него сама.

struct TaskStore {
    items: Vec<Task>,
    next_id: u64,
}

Конструктор и три метода - в одном impl TaskStore:

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

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

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

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

new создаёт пустое хранилище с next_id: 1. add принимает impl Into<Task> (M3.2): через From<&str> for Task из M3.1 подходит и &str; идентификатор присваивается внутри. get находит задачу по ID и возвращает ссылку - или TqError::NotFound, если такой нет. all возвращает срез &[Task] - вместо list, которая печатала сразу; теперь вызывающий сам решает, что делать со списком.

Итог

Свободные функции add, get, list уходят. main работает с TaskStore:

fn main() -> Result<(), TqError> {
    let mut store = TaskStore::new();

    store.add("Buy coffee")?;
    store.add("Buy milk")?;
    store.add("Buy eggs")?;

    for task in store.all() {
        println!("#{}: {} - {:?}", task.id, task.title, task.status);
    }

    match store.get(1) {
        Ok(task) => println!("found: #{}: {}", task.id, task.title),
        Err(e) => println!("error: {:?}", e),
    }

    match store.get(99) {
        Ok(task) => println!("#{}: {}", task.id, task.title),
        Err(TqError::NotFound(id)) => println!("task {} not found", id),
        Err(e) => println!("error: {:?}", e),
    }

    let task = &mut store.items[0];
    task.complete();
    println!("done: {}", task.is_done());

    match Task::new(0, "") {
        Ok(task) => store.add(task)?,
        Err(e) => println!("rejected: {:?}", e),
    }

    Ok(())
}

store.add("Buy coffee")? - &str передаётся напрямую: From<&str> и impl Into<Task> встретились здесь. Завершить задачу - так же, как в предыдущей главе, только tasks[0] стало store.items[0].

Два теста закрывают новое поведение:

#[test]
fn store_assigns_sequential_ids() {
    let mut store = TaskStore::new();
    store.add("A").unwrap();
    store.add("B").unwrap();
    assert_eq!(store.all()[0].id, 1);
    assert_eq!(store.all()[1].id, 2);
}

#[test]
fn store_get_returns_not_found() {
    let store = TaskStore::new();
    assert!(matches!(store.get(99), Err(TqError::NotFound(99))));
}

make ci проходит. Задачи живут в TaskStore - добавить, найти, перечислить. Хранилище знает о задачах всё; о том, что задачи бывают не единственным типом данных - пока ничего.

Полный код tq для этой главы - 3-a-voice/03-a-store-for-anything/.