← Оглавление

3.4 Хранилище для всего

Хорошая гильдия не полагается на память мастера. Она полагается на документ: изделие обязано принять, найти, перечислить - мастер подписался. Хранилище умеет всё нужное. Умение без подписанного обязательства принято называть надёжностью. Правильное слово - совпадение.

Что нужно

Мы только что создали TaskStore, где методы add, get, all живут в impl TaskStore. Но нигде не записано, что хранилище обязано их иметь - убери all, компилятор промолчит.

Нужен именованный контракт: описание того, что хранилище обязано уметь. Тогда реализация и требование живут отдельно - и несоответствие между ними видит компилятор, а не только тот, кто пишет код.

Сборка

Чтобы хранилище умело находить элемент по идентификатору, каждый элемент должен уметь его отдавать. Это минимальное требование ко всему, что хранилище принимает:

trait HasId {
    fn id(&self) -> u64;
}

Для Task:

impl HasId for Task {
    fn id(&self) -> u64 {
        self.id
    }
}

Несколько методов возвращают Result с одной и той же ошибкой - и это повторяется в сигнатурах подряд. Псевдоним убирает повторение:

type TqResult<T> = Result<T, TqError>;
Без псевдонимаС псевдонимом
fn add(...) -> Result<(), TqError>fn add(...) -> TqResult<()>
fn get(...) -> Result<&T, TqError>fn get(...) -> TqResult<&T>
Task::new(...) -> Result<Task, TqError>Task::new(...) -> TqResult<Task>

T - тип при успехе: TqResult<()> означает “ничего”, TqResult<&Task> - “ссылку на задачу”.

Контракт хранилища описывается трейтом. T - место для типа содержимого, : HasId говорит, что этот тип умеет отдавать идентификатор.

trait Store<T: HasId> {
    fn add(&mut self, item: impl Into<T>) -> TqResult<()>;
    fn get(&self, id: u64) -> TqResult<&T>;
    fn all(&self) -> &[T];
}

TaskStore реализует этот контракт. Методы переезжают из impl TaskStore в impl Store<Task> for TaskStore - impl TaskStore оставляет себе только конструктор:

impl TaskStore {
    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)  // HasId
            .ok_or(TqError::NotFound(id))
    }

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

impl Store<Task> for TaskStore - “TaskStore реализует контракт Store для задач”. Компилятор проверяет: все три метода есть, сигнатуры совпадают. В get вызывается task.id() - метод из HasId. В add поле task.id записывается напрямую: HasId предоставляет только чтение.

Итог

В main меняется только сигнатура - Result<(), TqError> заменяется на TqResult<()>. Остальное без правок:

fn main() -> TqResult<()> {

Существующие тесты проходят без правок. Это и есть их работа: убедиться, что внутренние изменения не сломали поведение снаружи.

make ci проходит. TaskStore подписался: impl Store<Task> for TaskStore - это не просто методы, это обязательство.

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


Lore: Ограничение на трейте

Store<T: HasId> - ограничение стоит прямо на параметре трейта. Нельзя реализовать Store<T> для типа без HasId; компилятор проверит это при попытке.

Когда ограничений несколько, запись T: HasId + Clone удлиняется. Тогда используют where:

impl<T> Store<T> for TaskStore
where
    T: HasId,
{
    // ...
}

Оба варианта компилируются одинаково. where удобнее при длинных ограничениях.