Добавление approve для изменения поведения content
Метод approve
будет похож на метод request_review
: он установит state
в значение, которое должно быть текущим состоянием, когда это состояние одобряется, как показано в Листинге 17-16.
Имя файла: src/lib.rs
impl Post {
--snip--
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
--snip--
fn approve(self: Box<Self>) -> Box<dyn State> {
1 self
}
}
struct PendingReview {}
impl State for PendingReview {
--snip--
fn approve(self: Box<Self>) -> Box<dyn State> {
2 Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
Листинг 17-16: Реализация метода approve
на Post
и трейте State
Мы добавляем метод approve
в трейт State
и добавляем новую структуру, которая реализует State
, состояние Published
.
Похожим образом, как работает request_review
на PendingReview
, если мы вызовем метод approve
на Draft
, это не будет иметь никакого эффекта, потому что approve
вернет self
[1]. Когда мы вызываем approve
на PendingReview
, оно возвращает новый, заboxed экземпляр структуры Published
[2]. Структура Published
реализует трейт State
, и для обоих методов request_review
и approve
она возвращает себя, потому что запись должна оставаться в состоянии Published
в этих случаях.
Теперь нам нужно обновить метод content
на Post
. Мы хотим, чтобы значение, возвращаемое из content
, зависело от текущего состояния Post
, поэтому мы будем делегировать Post
методу content
, определенному на его state
, как показано в Листинге 17-17.
Имя файла: src/lib.rs
impl Post {
--snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
--snip--
}
Листинг 17-17: Обновление метода content
на Post
для делегирования методу content
на State
Поскольку цель - сохранить все эти правила внутри структур, которые реализуют State
, мы вызываем метод content
на значении в state
и передаем экземпляр записи (то есть self
) в качестве аргумента. Затем мы возвращаем значение, возвращаемое при использовании метода content
на значении state
.
Мы вызываем метод as_ref
на Option
, потому что мы хотим получить ссылку на значение внутри Option
, а не владение значением. Поскольку state
является Option<Box<dyn State>>
, когда мы вызываем as_ref
, возвращается Option<&Box<dyn State>>
. Если бы мы не вызывали as_ref
, мы получили бы ошибку, потому что мы не можем переместить state
из заимствованной &self
параметра функции.
Затем мы вызываем метод unwrap
, который мы знаем, никогда не будет вызываться с ошибкой, потому что мы знаем, что методы на Post
гарантируют, что state
всегда будет содержать значение Some
после выполнения этих методов. Это один из случаев, о которых мы говорили в разделе "Случаи, когда у вас есть больше информации, чем компилятор", когда мы знаем, что значение None
никогда не может появиться, хотя компилятор не может понять это.
В этом случае, когда мы вызываем content
на &Box<dyn State>
, приведение типов по умолчанию будет действовать на &
и Box
, так что метод content
в конечном итоге будет вызываться на типе, который реализует трейт State
. Это означает, что нам нужно добавить content
в определение трейта State
, и именно здесь мы укажем логику для возврата содержания в зависимости от текущего состояния, как показано в Листинге 17-18.
Имя файла: src/lib.rs
trait State {
--snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
1 ""
}
}
--snip--
struct Published {}
impl State for Published {
--snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
2 &post.content
}
}
Листинг 17-18: Добавление метода content
в трейт State
Мы добавляем реализацию по умолчанию для метода content
, которая возвращает пустой срез строки [1]. Это означает, что нам не нужно реализовывать content
на структурах Draft
и PendingReview
. Структура Published
переопределит метод content
и вернет значение из post.content
[2].
Обратите внимание, что нам нужны аннотации времени жизни для этого метода, как мы обсуждали в главе 10. Мы принимаем ссылку на post
в качестве аргумента и возвращаем ссылку на часть этого post
, поэтому время жизни возвращаемой ссылки связано с временем жизни аргумента post
.
И мы закончили - все в Листинге 17-11 теперь работает! Мы реализовали паттерн состояния с правилами рабочего процесса записи блога. Логика, связанная с правилами, находится в объектах состояния, а не разбросана по всему Post
.
Почему не использовать enum?
Возможно, вы задавались вопросом, почему мы не использовали enum
с различными возможными состояниями записи в качестве вариантов. Это, конечно, возможное решение; попробуйте его и сравните конечные результаты, чтобы понять, какой вариант вам нравится больше! Одним из недостатков использования enum
является то, что каждый участок кода, который проверяет значение enum
, должен иметь match
-выражение или аналогичное для обработки каждого возможного варианта. Это может быть более повторяющимся, чем этот вариант с объектами трейтов.