Мы добавляем в
Post публичный метод с именем request_review
("запросить проверку"), который будет принимать изменяемую ссылку на self
. Затем мы вызываем внутренний метод request_review для текущего состояния
Post
, и этот второй метод request_review поглощает текущее состояние и возвращает новое состояние.
Мы добавляем метод request_review в типаж
State
; все типы, реализующие этот типаж,
теперь должны будут реализовать метод request_review
. Обратите внимание, что вместо self
,
&self или
&mut self в качестве первого параметра метода у нас указан self: Box
. Этот синтаксис означает, что метод действителен только при его вызове с обёрткой
Box
, содержащей наш тип. Этот синтаксис становится владельцем
Box
, делая старое состояние недействительным, поэтому значение состояния
Post может быть преобразовано в новое состояние.
Чтобы поглотить старое состояние, метод request_review должен стать владельцем значения состояния. Это место, где приходит на помощь тип
Option поля state записи
Post
: мы вызываем метод take
, чтобы забрать значение
Some из поля state и
оставить вместо него значение
None
, потому что Rust не позволяет иметь неинициализированные поля в структурах. Это позволяет перемещать значение state из
Post
, а не заимствовать его. Затем мы установим новое значение state как результат этой операции.
Нам нужно временно установить state в
None
, вместо того, чтобы установить его напрямую с помощью кода вроде self.state = self.state.request_review();
. Нам нужно завладеть значением поля state
. Это даст нам гарантию, что
Post не сможет использовать старое значение state после того, как мы преобразовали его в новое состояние.
Метод request_review в
Draft должен вернуть новый экземпляр новой структуры
PendingReview
, обёрнутый в Box. Эта структура будет представлять состояние, в котором запись ожидает проверки. Структура
PendingReview также реализует метод request_review
, но не выполняет никаких преобразований. Она возвращает сама себя,
потому что, когда мы запрашиваем проверку записи, уже находящейся в состоянии
PendingReview
, она всё так же должна продолжать оставаться в состоянии
PendingReview
Теперь мы начинаем видеть преимущества шаблона "Состояние": метод request_review для
Post одинаков, он не зависит от значения state
. Каждое состояние само несёт ответственность за свои действия.
Оставим метод content у
Post таким как есть, возвращающим пустой фрагмент строки.
Теперь мы можем иметь
Post как в состоянии
PendingReview
, так и в состоянии
Draft
,
но мы хотим получить такое же поведение в состоянии
PendingReview
. Листинг 17-11
теперь работает до строки 10!
Добавление approve для изменения поведения content
Метод approve
("одобрить") будет аналогичен методу request_review
: он будет устанавливать у state значение, которое должна иметь запись при её одобрении, как показано в листинге 17-16:
Файл: src/lib.rs
Листинг 17-16. Реализация метода
approve
для типа
Post
и типажа
State 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> { self
}
} struct
PendingReview
{} impl
State for
PendingReview {
// --snip-- fn approve
(
self
:
Box
<
Self
>) ->
Box
<
dyn
State> {
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
}
}
Мы добавляем метод approve в типаж
State
, добавляем новую структуру, которая реализует этот типаж
State и структуру для состояния
Published
Подобно тому, как работает request_review для
PendingReview
, если мы вызовем метод approve для
Draft
, он не будет иметь никакого эффекта, потому что approve вернёт self
. Когда мы вызываем для
PendingReview метод approve
, то он возвращает новый упакованный экземпляр структуры
Published
. Структура
Published реализует трейт
State
, и как для метода request_review
, так и для метода approve она возвращает себя,
потому что в этих случаях запись должна оставаться в состоянии
Published
Теперь нам нужно обновить метод content для
Post
. Мы хотим, чтобы значение,
возвращаемое из content
, зависело от текущего состояния
Post
, поэтому мы собираемся перенести часть функциональности
Post в метод content
, заданный для state
, как показано в листинге 17.17:
Файл: src/lib.rs
Листинг 17-17: Обновление метода
content
в структуре
Post
для делегирования части функциональности
методу
content
структуры
State
Поскольку наша цель состоит в том, чтобы сохранить все эти действия внутри структур,
реализующих типаж
State
, мы вызываем метод content у значения в поле state и
передаём экземпляр публикации (то есть self
) в качестве аргумента. Затем мы возвращаем значение, которое нам выдаёт вызов метода content поля state
Мы вызываем метод as_ref у
Option
, потому что нам нужна ссылка на значение внутри
Option
, а не владение значением. Поскольку state является типом
OptionState>>
, то при вызове метода as_ref возвращается
Option<&Box>
. Если бы мы не вызывали as_ref
, мы бы получили ошибку, потому что мы не можем переместить state из заимствованного параметра
&self функции.
Затем мы вызываем метод unwrap
. Мы знаем, что этот метод здесь никогда не приведёт к аварийному завершению программы, так все методы
Post устроены таким образом,
что после их выполнения, в поле state всегда содержится значение
Some
. Это один из случаев, про которых мы говорили в разделе "Случаи, когда у вас больше информации,
чем у компилятора"
главы 9 - случай, когда мы знаем, что значение
None никогда не встретится, даже если компилятор не может этого понять.
impl
Post {
// --snip-- pub fn content
(&
self
) -> &
str
{ self
.state.as_ref().unwrap().content(
self
)
}
// --snip--
}
Теперь, когда мы вызываем content у типа
&Box
, в действие вступает принудительное приведение (deref coercion) для
&
и
Box
, поэтому в конечном итоге метод content будет вызван для типа, который реализует типаж
State
. Это означает,
что нам нужно добавить метод content в определение типажа
State
, и именно там мы поместим логику для определения того, какое содержимое возвращать, в зависимости от текущего состояния, как показано в листинге 17-18:
Файл: src/lib.rs
Листинг 17-18. Добавление метода
content
в трейт
State
Мы добавляем реализацию по умолчанию метода content
, который возвращает пустой фрагмент строки. Это означает, что нам не придётся реализовывать content в
структурах
Draft и
PendingReview
. Структура
Published будет переопределять метод content и вернёт значение из post.content
Обратите внимание, что для этого метода нам нужны аннотации времени жизни, как мы обсуждали в главе 10. Мы берём ссылку на post в качестве аргумента и возвращаем ссылку на часть этого post
, поэтому время жизни возвращённой ссылки связано с временем жизни аргумента post
И вот, мы закончили - теперь всё из листинга 17-11 работает! Мы реализовали шаблон "Состояние", определяющий правила процесса работы с записью в блоге. Логика,
связанная с этими правилами, находится в объектах состояний, а не разбросана по всей структуре
Post
Почему не перечисление?
Возможно, вам было интересно, почему мы не использовали enum с различными возможными состояниями записи в качестве вариантов. Это, безусловно, одно из возможных решений. Попробуйте его реализовать и сравните конечные trait
State
{
// --snip-- fn content
<
'a
>(&
self
, post: &
'a
Post) -> &
'a str
{
""
}
}
// --snip-- struct
Published
{} impl
State for
Published {
// --snip-- fn content
<
'a
>(&
self
, post: &
'a
Post) -> &
'a str
{
&post.content
}
}
результаты, чтобы выбрать, какой из вариантов вам больше нравится! Одним из недостатков использования перечисления является то, что в каждом месте, где проверяется значение перечисления, потребуется выражение match или что-то подобное для обработки всех возможных вариантов. Возможно в этом случае нам придётся повторять больше кода, чем это было в решении с типаж-объектом.
Компромиссы шаблона "Состояние"
Мы показали, что Rust способен реализовать объектно-ориентированный шаблон "Состояние" для инкапсуляции различных типов поведения, которые должна иметь запись в каждом состоянии. Методы в
Post ничего не знают о различных видах поведения. При такой организации кода, нам достаточно взглянуть только на один его участок, чтобы узнать отличия в поведении опубликованной публикации: в реализацию типажа
State у структуры
Published
Если бы мы собирались создать альтернативную реализацию, не использующую шаблон "Состояние", мы могли бы использовать выражения match в методах структуры
Post или даже в коде main
, для проверки состояния записи и изменения её поведения в этих местах. Это означало бы, что нам пришлось бы анализировать несколько участков кода,
чтобы понять что как ведёт себя сообщение в опубликованном состоянии! Если бы мы решили добавить ещё состояний, стало бы ещё хуже: каждому этих выражений match потребовались бы дополнительные ответвления.
С помощью шаблона "Состояние" методы
Post и участки, где мы используем
Post
, не требуют использования выражений match
, а для добавления нового состояния нужно только добавить новую структуру и реализовать методы типажа у одной этой структуры.
Реализацию с использованием шаблона "Состояние" легко расширить для добавления новой функциональности. Чтобы увидеть, как легко поддерживать код, использующий данный шаблон, попробуйте выполнить некоторые из предложений ниже:
Добавьте метод reject
, который изменяет состояние публикации с
PendingReview обратно на
Draft
Потребуйте два вызова метода approve
, прежде чем переводить состояние в
Published
Разрешите пользователям добавлять текстовое содержимое только тогда, когда публикация находится в состоянии
Draft
. Подсказка: пусть объект состояния решает, можно ли менять содержимое, но не отвечает за изменение
Post
Одним из недостатков шаблона "Состояние" является то, что поскольку состояния сами реализуют переходы между собой, некоторые из состояний получаются связанными друг с другом. Если мы добавим другое состояние между
PendingReview и
Published
,
например
Scheduled
("запланировано"), то придётся изменить код в
PendingReview
,
чтобы оно теперь переходило в
Scheduled
. Если бы не нужно было менять
PendingReview при добавлении нового состояния, было бы меньше работы, но это означало бы, что мы переходим на другой шаблон проектирования.
Другим недостатком является то, что мы продублировали некоторую логику. Чтобы устранить некоторое дублирование, мы могли бы попытаться сделать реализации по умолчанию для методов request_review и approve типажа
State
, которые возвращают self
; однако это нарушило бы безопасность объекта, потому что типаж не знает, каким конкретно будет self
. Мы хотим иметь возможность использовать
State в качестве типаж-объекта, поэтому нам нужно, чтобы его методы были объектно-безопасными.
Другое дублирование включает в себя схожие реализации методов request_review и approve у
Post
. Оба метода делегируют реализации одного и того же метода значению поля state типа
Option и устанавливают результатом новое значение поля state
. Если бы у
Post было много методов, которые следовали этому шаблону, мы могли бы рассмотреть определение макроса для устранения повторения (смотри раздел "Макросы"
в главе 19).
Реализуя шаблон "Состояние" точно так, как он определён для объектно- ориентированных языков, мы не настолько полно используем преимущества Rust, как могли бы. Давайте посмотрим на некоторые изменения, которые мы можем внести в крейт blog
, чтобы недопустимые состояния и переходы превратить в ошибки времени компиляции.
1 ... 44 45 46 47 48 49 50 51 ... 62
Кодирование состояний и поведения в виде типовМы покажем вам, как переосмыслить шаблон "Состояние", чтобы получить другой набор компромиссов. Вместо того, чтобы полностью инкапсулировать состояния и переходы,так, чтобы внешний код не знал о них, мы будем кодировать состояния с помощью разных типов. Следовательно, система проверки типов Rust предотвратит попытки использовать черновые публикации, там где разрешены только опубликованные публикации, вызывая ошибки компиляции.Давайте рассмотрим первую часть main в листинге 17-11:Файл: src/main.rsМы по-прежнему поддерживаем создание новых сообщений в состоянии "черновика" с помощью метода Post::new и возможность добавлять текст к содержимому публикации.Но вместо метода content у чернового сообщения, возвращающего пустую строку, мы сделаем так, что у черновых сообщений вообще не будет метода content. Таким fn main() { let mut post = Post::new(); post.add_text("I ate a salad for lunch today"); assert_eq!("", post.content()); }
образом, если мы попытаемся получить содержимое черновика, мы получим ошибку компилятора, сообщающую, что метод не существует. В результате мы не сможем случайно отобразить черновик содержимого записи в работающей программе, потому что этот код даже не скомпилируется. В листинге 17-19 показано определение структур
Post и
DraftPost
, а также методов для каждой из них:
Файл: src/lib.rs
Листинг 17-19: Структура
Post
с методом
content
и структура
DraftPost
без метода
content
Обе структуры,
Post и
DraftPost
, имеют приватное поле content
, в котором хранится текст сообщения блога. Структуры больше не содержат поле state
, потому что мы перемещаем кодирование состояния в типы структур. Структура
Post будет представлять опубликованную публикацию, и у неё есть метод content
, который возвращает content
У нас все ещё есть функция
Post::new
, но вместо возврата экземпляра
Post она возвращает экземпляр
DraftPost
. Поскольку поле content является приватным и нет никаких функций, которые возвращают
Post
, просто так создать экземпляр
Post уже невозможно.
Структура
DraftPost имеет метод add_text
, поэтому мы можем добавлять текст к content как и раньше, но учтите, что в
DraftPost не определён метод content
! Так что теперь программа гарантирует, что все записи начинаются как черновики, а черновики pub struct
Post
{ content:
String
,
} pub struct
DraftPost
{ content:
String
,
} impl
Post { pub fn new
() -> DraftPost {
DraftPost { content:
String
::new(),
}
} pub fn content
(&
self
) -> &
str
{
&
self
.content
}
} impl
DraftPost { pub fn add_text
(&
mut self
, text: &
str
) { self
.content.push_str(text);
}
}