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 удобнее при длинных ограничениях.