3.3 Свой ящик
Ящик строят не потому, что навсегда. Его строят потому, что рассыпанное неудобно - а правильный ящик задаёт форму даже тогда, когда сам в итоге меняется. Это принято называть временным решением. Правильное слово - первый шаг.
Что нужно
В текущем решении у нас две явные проблемы:
Идентификатор - что-то уникальное, однако main сам решает, какой назначить. Пока задач
пара штук - легко отследить вручную. Пропущенный или повторный номер компилятор не
заметит; нужен единый центр для выдачи номеров.
Три функции: add, get и list - которые всегда ходят вместе. У каждой первый аргумент
- либо
&mut Vec<Task>, либо&[Task]. Список задач - их общее состояние; он живёт вmainи передаётся вручную при каждом вызове.
Это работает, пока функций три и мастер один.
Сборка
Перенесём состояние внутрь: пусть структура владеет списком и отвечает за него сама.
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/.