ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1158
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
В листинге 17-11 показано, что мы хотим иметь возможность вызывать метод add_text и
передать ему
&str
, которое добавляется к текстовому содержимому записи блога. Мы реализуем эту возможность как метод, а не делаем поле content публично доступным,
используя pub
. Это означает, что позже мы сможем написать метод, который будет контролировать, как именно читаются данные из поля content
. Метод add_text довольно прост, поэтому давайте добавим его реализацию в блок impl Post листинга 17-
13 :
Файл: src/lib.rs
Листинг 17-13. Реализация
add_text
для добавления текста к
content
(содержимому записи)
Метод add_text принимает изменяемую ссылку на self
, потому что мы меняем экземпляр
Post
, для которого вызываем add_text
. Затем мы вызываем push_str для
String у поля content и передаём text аргументом для добавления к сохранённому content
. Это поведение не зависит от состояния, в котором находится запись, таким образом оно не является частью шаблона "Состояние". Метод add_text вообще не взаимодействует с полем state
, но это часть поведения, которое мы хотим поддерживать.
Убедимся, что содержание черновика будет пустым
Даже после того, как мы вызвали add_text и добавили некоторый контент в нашу запись, мы хотим, чтобы метод content возвращал пустой фрагмент строки, так как запись всё ещё находится в черновом состоянии, как это показано в строке 7 листинга
17-11. А пока давайте реализуем метод content наиболее простым способом, который будет удовлетворять этому требованию: будем всегда возвращать пустой фрагмент строки. Мы изменим код позже, как только реализуем возможность изменить состояние записи, чтобы она могла бы быть опубликована. Пока что записи могут находиться только в черновом состоянии, поэтому содержимое записи всегда должно быть пустым.
Листинг 17-14 показывает такую реализацию-заглушку:
Файл: src/lib.rs impl
Post {
// --snip-- pub fn add_text
(&
mut self
, text: &
str
) { self
.content.push_str(text);
}
}
Листинг 17-14. Добавление реализации-заглушки для метода
content
в
Post
, которая всегда возвращает
пустой фрагмент строки.
С добавленным таким образом методом content всё в листинге 17-11 работает, как задумано, вплоть до строки 7.
Запрос на проверку записи меняет её состояние
Далее нам нужно добавить функциональность для запроса проверки записи, который должен изменить её состояние с
Draft на
PendingReview
. Листинг 17-15 показывает такой код:
Файл: src/lib.rs
Листинг 17-15. Реализация методов
request_review
в структуре
Post
и типаже
State impl
Post {
// --snip-- pub fn content
(&
self
) -> &
str
{
""
}
} impl
Post {
// --snip-- pub fn request_review
(&
mut self
) { if let
Some
(s) = self
.state.take() { self
.state =
Some
(s.request_review())
}
}
} trait
State
{ fn request_review
(
self
:
Box
<
Self
>) ->
Box
<
dyn
State>;
} struct
Draft
{} impl
State for
Draft { fn request_review
(
self
:
Box
<
Self
>) ->
Box
<
dyn
State> {
Box
::new(PendingReview {})
}
} struct
PendingReview
{} impl
State for
PendingReview { fn request_review
(
self
:
Box
<
Self
>) ->
Box
<
dyn
State> { self
}
}
Мы добавляем в
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 является типом
Option
, то при вызове метода 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
. Если бы не нужно было менять
Компромиссы шаблона "Состояние"
Мы показали, что 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