Реализация объектно-ориентированного шаблона проектирования
Шаблон состояния (state pattern) — это объектно-ориентированный шаблон проектирования. Суть шаблона в том, что мы определяем набор состояний, которые значение может иметь внутренне. Состояния представлены набором объектов-состояний, и поведение значения изменяется в зависимости от его состояния. Мы рассмотрим пример структуры BlogPost, которая имеет поле для хранения своего состояния, которое будет объектом состояния из набора «черновик», «рецензирование» или «опубликовано».
Объекты состояний разделяют функциональность: в Rust, конечно, мы используем структуры и трейты, а не объекты и наследование. Каждый объект состояния отвечает за свое собственное поведение и за управление тем, когда он должен измениться в другое состояние. Значение, которое содержит объект состояния, ничего не знает о различном поведении состояний или о том, когда переходить между состояниями.
Преимущество использования шаблона состояния в том, что когда бизнес-требования программы изменяются, нам не нужно изменять код значения, содержащего состояние, или код, использующего значение. Нам нужно будет только обновить код внутри одного из объектов состояния, чтобы изменить его правила или, возможно, добавить больше объектов состояния.
Сначала мы реализуем шаблон состояния более традиционным объектно-ориентированным способом. Затем мы используем подход, который более естественен для Rust. Давайте углубимся в поэтапную реализацию рабочего процесса блога с использованием шаблона состояния.
Конечная функциональность будет выглядеть так:
- Запись в блоге начинается как пустой черновик.
- Когда черновик готов, запрашивается рецензирование записи.
- Когда запись утверждена, она публикуется.
- Только опубликованные записи блога возвращают содержимое для печати, чтобы неутвержденные записи не могли быть случайно опубликованы.
Любые другие попытки изменения записи не должны иметь эффекта. Например, если мы попытаемся утвердить черновик записи блога до того, как запросили рецензирование, запись должна остаться неопубликованным черновиком.
Попытка традиционного объектно-ориентированного стиля
Существует бесконечное множество способов структурировать код для решения одной и той же проблемы, каждый со своими компромиссами. Реализация в этом разделе больше соответствует традиционному объектно-ориентированному стилю, который можно написать на Rust, но который не использует некоторые сильные стороны Rust. Позже мы продемонстрируем другое решение, которое все еще использует объектно-ориентированный шаблон проектирования, но структурировано таким образом, что может показаться менее familiar программистам с объектно-ориентированным опытом. Мы сравним два решения, чтобы ощутить компромиссы при проектировании кода на Rust иначе, чем кода на других языках.
Листинг 18-11 показывает этот рабочий процесс в виде кода: это пример использования API, который мы реализуем в библиотечном крейте с именем blog. Это пока не скомпилируется, потому что мы еще не реализовали крейт blog.
Файл: src/main.rs
// Этот код не компилируется! use blog::Post; fn main() { let mut post = Post::new(); post.add_text("I ate a salad for lunch today"); assert_eq!("", post.content()); post.request_review(); assert_eq!("", post.content()); post.approve(); assert_eq!("I ate a salad for lunch today", post.content()); }
Листинг 18-11: Код, демонстрирующий желаемое поведение, которое мы хотим иметь в нашем крейте blog
Мы хотим позволить пользователю создавать новый черновик записи блога с помощью Post::new. Мы хотим разрешить добавление текста в запись блога. Если мы попытаемся получить содержимое записи сразу, до утверждения, мы не должны получить никакого текста, потому что запись все еще является черновиком. Мы добавили assert_eq! в код для демонстрационных целей. Отличным модульным тестом для этого было бы утверждение, что черновик записи блога возвращает пустую строку из метода content, но мы не будем писать тесты для этого примера.
Далее мы хотим включить запрос на рецензирование записи, и мы хотим, чтобы content возвращал пустую строку во время ожидания рецензии. Когда запись получает одобрение, она должна быть опубликована, то есть текст записи будет возвращен при вызове content.
Обратите внимание, что единственный тип, с которым мы взаимодействуем из крейта, — это тип Post. Этот тип будет использовать шаблон состояния и будет содержать значение, которое будет одним из трех объектов состояния, представляющих различные состояния, в которых может находиться запись — черновик, рецензирование или опубликовано. Изменение из одного состояния в другое будет управляться внутренне в типе Post. Состояния изменяются в ответ на методы, вызываемые пользователями нашей библиотеки на экземпляре Post, но им не нужно напрямую управлять изменениями состояния. Кроме того, пользователи не могут ошибиться с состояниями, например, опубликовать запись до ее рецензирования.
Определение Post и создание нового экземпляра
Давайте начнем реализацию библиотеки! Мы знаем, что нам нужна публичная структура Post, которая содержит некоторый контент, поэтому мы начнем с определения структуры и связанной с ней публичной функции new для создания экземпляра Post, как показано в листинге 18-12. Мы также создадим приватный трейт State, который будет определять поведение, которое должны иметь все объекты состояния для Post.
Затем Post будет содержать трейт-объект Box<dyn State> внутри Option<T> в приватном поле с именем state для хранения объекта состояния. Вы скоро увидите, почему Option<T> необходим.
Файл: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { state: Option<Box<dyn State>>, content: String, } impl Post { pub fn new() -> Post { Post { state: Some(Box::new(Draft {})), content: String::new(), } } } trait State {} struct Draft {} impl State for Draft {} }
Листинг 18-12: Определение структуры Post и функции new, которая создает новый экземпляр Post, трейта State и структуры Draft
Трейт State определяет поведение, общее для различных состояний записи. Объекты состояний — это Draft, PendingReview и Published, и все они будут реализовывать трейт State. Пока что трейт не имеет никаких методов, и мы начнем с определения только состояния Draft, потому что это состояние, в котором мы хотим, чтобы запись начинала.
Когда мы создаем новый Post, мы устанавливаем его поле state в значение Some, содержащее Box. Этот Box указывает на новый экземпляр структуры Draft. Это гарантирует, что всякий раз, когда мы создаем новый экземпляр Post, он будет начинаться как черновик. Поскольку поле state структуры Post является приватным, нет способа создать Post в любом другом состоянии! В функции Post::new мы устанавливаем поле content в новую пустую String.
Хранение текста содержимого записи
Мы видели в листинге 18-11, что мы хотим иметь возможность вызывать метод с именем add_text и передавать ему &str, который затем добавляется как текстовое содержимое записи блога. Мы реализуем это как метод, а не выставляем поле content как pub, чтобы позже мы могли реализовать метод, который будет контролировать, как читаются данные поля content. Метод add_text довольно прост, поэтому давайте добавим реализацию из листинга 18-13 в блок impl Post.
Файл: src/lib.rs
#![allow(unused)] fn main() { impl Post { // --snip-- pub fn add_text(&mut self, text: &str) { self.content.push_str(text); } } }
Листинг 18-13: Реализация метода add_text для добавления текста в содержимое записи
Метод add_text принимает изменяемую ссылку на self, потому что мы изменяем экземпляр Post, для которого вызываем add_text. Затем мы вызываем push_str для String в content и передаем аргумент text для добавления к сохраненному содержимому. Это поведение не зависит от состояния записи, поэтому оно не является частью шаблона состояния. Метод add_text вообще не взаимодействует с полем state, но является частью поведения, которое мы хотим поддерживать.
Гарантия того, что содержимое черновика записи пусто
Даже после того как мы вызвали add_text и добавили некоторое содержимое в нашу запись, мы все равно хотим, чтобы метод content возвращал пустой строковый срез, потому что запись все еще находится в состоянии черновика, как показано первым assert_eq! в листинге 18-11. Пока давайте реализуем метод content самым простым способом, который будет удовлетворять этому требованию: всегда возвращать пустой строковый срез. Мы изменим это позже, как только реализуем возможность изменять состояние записи, чтобы ее можно было опубликовать. До сих пор записи могут быть только в состоянии черновика, поэтому содержимое записи должно всегда быть пустым. Листинг 18-14 показывает эту заглушку реализации.
Файл: src/lib.rs
#![allow(unused)] fn main() { impl Post { // --snip-- pub fn content(&self) -> &str { "" } } }
Листинг 18-14: Добавление реализации-заглушки для метода content в Post, которая всегда возвращает пустой строковый срез
С добавленным методом content все в листинге 18-11 до первого assert_eq! работает как задумано.
Запрос рецензирования, который изменяет состояние записи
Далее нам нужно добавить функциональность для запроса рецензирования записи, которая должна изменять ее состояние с Draft на PendingReview. Листинг 18-15 показывает этот код.
Файл: src/lib.rs
#![allow(unused)] fn main() { 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 } } }
Листинг 18-15: Реализация методов request_review для Post и трейта State
Мы даем Post публичный метод request_review, который будет принимать изменяемую ссылку на self. Затем мы вызываем внутренний метод request_review для текущего состояния Post, и этот второй метод request_review потребляет текущее состояние и возвращает новое состояние.
Мы добавляем метод request_review в трейт State; все типы, реализующие этот трейт, теперь должны будут реализовать метод request_review. Обратите внимание, что вместо того, чтобы иметь self, &self или &mut self в качестве первого параметра метода, мы имеем self: Box<Self>. Этот синтаксис означает, что метод действителен только при вызове на Box, содержащем тип. Этот синтаксис забирает владение Box<Self>, делая недействительным старое состояние, чтобы значение состояния Post могло преобразоваться в новое состояние.
Чтобы потребить старое состояние, методу request_review нужно взять владение значением состояния. Здесь пригождается Option в поле state структуры Post: мы вызываем метод take, чтобы извлечь значение Some из поля state и оставить None на его месте, потому что Rust не позволяет нам иметь незаполненные поля в структурах. Это позволяет нам переместить значение состояния из Post, а не заимствовать его. Затем мы установим значение состояния записи в результат этой операции.
Нам нужно временно установить state в None, а не устанавливать его напрямую с помощью кода типа self.state = self.state.request_review();, чтобы получить владение значением состояния. Это гарантирует, что Post не сможет использовать старое значение состояния после того, как мы преобразовали его в новое состояние.
Метод request_review для Draft возвращает новый упакованный экземпляр новой структуры PendingReview, которая представляет состояние, когда запись ожидает рецензирования. Структура PendingReview также реализует метод request_review, но не выполняет никаких преобразований. Вместо этого она возвращает себя, потому что когда мы запрашиваем рецензирование для записи, уже находящейся в состоянии PendingReview, она должна остаться в состоянии PendingReview.
Теперь мы начинаем видеть преимущества шаблона состояния: метод request_review для Post одинаков независимо от значения его состояния. Каждое состояние отвечает за свои собственные правила.
Мы оставим метод content в Post как есть, возвращающий пустой строковый срез. Теперь мы можем иметь Post в состоянии PendingReview, а также в состоянии Draft, но мы хотим одинаковое поведение в состоянии PendingReview. Листинг 18-11 теперь работает до второго вызова assert_eq!!
Добавление approve для изменения поведения content
Метод approve будет похож на метод request_review: он установит state в значение, которое должно быть у текущего состояния, когда это состояние утверждено, как показано в листинге 18-16.
Файл: src/lib.rs
#![allow(unused)] fn main() { 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 } } }
Листинг 18-16: Реализация метода approve для Post и трейта State
Мы добавляем метод approve в трейт State и добавляем новую структуру, реализующую State - состояние Published.
Подобно тому, как работает request_review для PendingReview, если мы вызовем метод approve для Draft, это не будет иметь эффекта, потому что approve вернет self. Когда мы вызываем approve для PendingReview, он возвращает новый упакованный экземпляр структуры Published. Структура Published реализует трейт State, и для обоих методов request_review и approve возвращает себя, потому что запись должна оставаться в состоянии Published в этих случаях.
Теперь нам нужно обновить метод content для Post. Мы хотим, чтобы значение, возвращаемое из content, зависело от текущего состояния Post, поэтому мы поручим Post делегировать методу content, определенному в его состоянии, как показано в листинге 18-17.
Файл: src/lib.rs
#![allow(unused)] fn main() { // Этот код не компилируется! impl Post { // --snip-- pub fn content(&self) -> &str { self.state.as_ref().unwrap().content(self) } // --snip-- } }
Листинг 18-17: Обновление метода content в Post для делегирования методу content в State
Поскольку цель состоит в том, чтобы держать все эти правила внутри структур, реализующих State, мы вызываем метод content для значения в state и передаем экземпляр записи (то есть self) в качестве аргумента. Затем мы возвращаем значение, которое возвращается из использования метода content для значения состояния.
Мы вызываем метод as_ref для Option, потому что хотим получить ссылку на значение внутри Option, а не владение значением. Поскольку state имеет тип Option<Box<dyn State>>, когда мы вызываем as_ref, возвращается Option<&Box<dyn State>>. Если бы мы не вызвали as_ref, мы получили бы ошибку, потому что не можем переместить state из заимствованного &self параметра функции.
Затем мы вызываем метод unwrap, который, как мы знаем, никогда не вызовет панику, потому что мы знаем, что методы в Post гарантируют, что state всегда будет содержать значение Some, когда эти методы завершатся. Это один из случаев, о которых мы говорили в разделе "Когда у вас больше информации, чем у компилятора" в Главе 9, когда мы знаем, что значение None никогда невозможно, даже если компилятор не может это понять.
В этот момент, когда мы вызываем content на &Box<dyn State>, приведение разыменования (deref coercion) подействует на & и Box, так что метод content в конечном итоге будет вызван для типа, реализующего трейт State. Это означает, что нам нужно добавить content в определение трейта State, и именно там мы разместим логику для определения того, какой контент возвращать в зависимости от того, какое состояние у нас есть, как показано в листинге 18-18.
Файл: src/lib.rs
#![allow(unused)] fn main() { 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 } } }
Листинг 18-18: Добавление метода content в трейт State
Мы добавляем реализацию по умолчанию для метода content, которая возвращает пустой строковый срез. Это означает, что нам не нужно реализовывать content для структур Draft и PendingReview. Структура Published переопределит метод content и вернет значение из post.content. Хотя это удобно, наличие метода content в State, определяющего содержимое Post, размывает границы ответственности между State и Post.
Обратите внимание, что нам нужны аннотации времени жизни для этого метода, как мы обсуждали в Главе 10. Мы берем ссылку на запись в качестве аргумента и возвращаем ссылку на часть этой записи, поэтому время жизни возвращаемой ссылки связано с временем жизни аргумента записи.
И мы закончили - весь листинг 18-11 теперь работает! Мы реализовали шаблон состояния с правилами рабочего процесса записи блога. Логика, связанная с правилами, живет в объектах состояний, а не разбросана по всему Post.
Почему не перечисление?
Вы могли задаться вопросом, почему мы не использовали перечисление с различными возможными состояниями записи в качестве вариантов. Это certainly возможное решение; попробуйте его и сравните конечные результаты, чтобы увидеть, что вам больше нравится! Один недостаток использования перечисления заключается в том, что каждое место, которое проверяет значение перечисления, будет нуждаться в выражении
matchили подобном, чтобы обработать каждый возможный вариант. Это может стать более повторяющимся, чем это решение с трейт-объектами.
Оценка шаблона состояния
Мы показали, что Rust способен реализовать объектно-ориентированный шаблон состояния для инкапсуляции различных видов поведения, которые запись должна иметь в каждом состоянии. Методы в Post ничего не знают о различных поведениях. Благодаря тому, как мы организовали код, нам нужно смотреть только в одно место, чтобы узнать о различных способах поведения опубликованной записи: реализацию трейта State для структуры Published.
Если бы мы создали альтернативную реализацию, которая не использует шаблон состояния, мы могли бы вместо этого использовать выражения match в методах Post или даже в основном коде, который проверяет состояние записи и изменяет поведение в этих местах. Это означало бы, что нам пришлось бы смотреть в несколько мест, чтобы понять все последствия нахождения записи в опубликованном состоянии.
С шаблоном состояния методы Post и места, где мы используем Post, не нуждаются в выражениях match, и чтобы добавить новое состояние, нам нужно только добавить новую структуру и реализовать методы трейта для этой одной структуры в одном месте.
Реализация с использованием шаблона состояния легко расширяется для добавления дополнительной функциональности. Чтобы увидеть простоту поддержки кода, использующего шаблон состояния, попробуйте несколько из этих предложений:
- Добавьте метод
reject, который изменяет состояние записи сPendingReviewобратно наDraft. - Требуйте два вызова
approveперед тем, как состояние может быть изменено наPublished. - Разрешите пользователям добавлять текстовое содержимое только тогда, когда запись находится в состоянии
Draft. Подсказка: пусть объект состояния отвечает за то, что может измениться в содержимом, но не отвечает за изменениеPost.
Один недостаток шаблона состояния заключается в том, что, поскольку состояния реализуют переходы между состояниями, некоторые состояния связаны друг с другом. Если мы добавим другое состояние между PendingReview и Published, такое как Scheduled, нам придется изменить код в PendingReview для перехода к Scheduled вместо этого. Было бы меньше работы, если бы PendingReview не нужно было изменять с добавлением нового состояния, но это означало бы переход к другому шаблону проектирования.
Другой недостаток заключается в том, что мы продублировали некоторую логику. Чтобы устранить некоторое дублирование, мы могли бы попытаться сделать реализации по умолчанию для методов request_review и approve в трейте State, которые возвращают self. Однако это не сработает: при использовании State как трейт-объекта, трейт не знает, каким именно будет конкретный self, поэтому тип возвращаемого значения неизвестен во время компиляции. (Это одно из правил совместимости с dyn, упомянутых ранее.)
Другое дублирование включает похожие реализации методов request_review и approve в Post. Оба метода используют Option::take с полем state структуры Post, и если state является Some, они делегируют реализации того же метода в обернутом значении и устанавливают новое значение поля state в результат. Если бы у нас было много методов в Post, которые следуют этому шаблону, мы могли бы рассмотреть возможность определения макроса для устранения повторения (см. раздел "Макросы" в Главе 20).
Реализуя шаблон состояния точно так, как он определен для объектно-ориентированных языков, мы не используем все преимущества Rust в полной мере. Давайте посмотрим на некоторые изменения, которые мы можем внести в крейт blog, чтобы сделать недопустимые состояния и переходы ошибками времени компиляции.
Кодирование состояний и поведения как типов
Мы покажем вам, как переосмыслить шаблон состояния, чтобы получить другой набор компромиссов. Вместо того чтобы полностью инкапсулировать состояния и переходы, чтобы внешний код не имел о них знаний, мы закодируем состояния в разные типы. Следовательно, система проверки типов Rust предотвратит попытки использования черновиков записей там, где разрешены только опубликованные записи, выдавая ошибку компилятора.
Давайте рассмотрим первую часть main из листинга 18-11:
Файл: src/main.rs
fn main() { let mut post = Post::new(); post.add_text("I ate a salad for lunch today"); assert_eq!("", post.content()); }
Мы все еще разрешаем создание новых записей в состоянии черновика с помощью Post::new и возможность добавлять текст в содержимое записи. Но вместо того, чтобы иметь метод content для черновика записи, который возвращает пустую строку, мы сделаем так, чтобы черновики записей вообще не имели метода content. Таким образом, если мы попытаемся получить содержимое черновика записи, мы получим ошибку компилятора, сообщающую, что метод не существует. В результате мы не сможем случайно отобразить содержимое черновика записи в рабочей среде, потому что этот код даже не скомпилируется. Листинг 18-19 показывает определение структуры Post и структуры DraftPost, а также методы для каждой.
Файл: src/lib.rs
#![allow(unused)] fn main() { 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); } } }
Листинг 18-19: Post с методом content и DraftPost без метода content
И структура Post, и структура DraftPost имеют приватное поле content, в котором хранится текст записи блога. Структуры больше не имеют поля state, потому что мы перемещаем кодирование состояния в типы структур. Структура Post будет представлять опубликованную запись и имеет метод content, который возвращает содержимое.
У нас все еще есть функция Post::new, но вместо возврата экземпляра Post она возвращает экземпляр DraftPost. Поскольку content является приватным и нет никаких функций, возвращающих Post, сейчас невозможно создать экземпляр Post.
Структура DraftPost имеет метод add_text, поэтому мы можем добавлять текст в content как и раньше, но обратите внимание, что в DraftPost не определен метод content! Теперь программа гарантирует, что все записи начинаются как черновики, и черновики записей не имеют доступного для отображения содержимого. Любая попытка обойти эти ограничения приведет к ошибке компилятора.
Итак, как мы получаем опубликованную запись? Мы хотим обеспечить правило, что черновик записи должен быть рецензирован и утвержден перед публикацией. Запись в состоянии ожидания рецензии все еще не должна отображать какое-либо содержимое. Давайте реализуем эти ограничения, добавив еще одну структуру PendingReviewPost, определив метод request_review в DraftPost для возврата PendingReviewPost и определив метод approve в PendingReviewPost для возврата Post, как показано в листинге 18-20.
Файл: src/lib.rs
#![allow(unused)] fn main() { impl DraftPost { // --snip-- pub fn request_review(self) -> PendingReviewPost { PendingReviewPost { content: self.content, } } } pub struct PendingReviewPost { content: String, } impl PendingReviewPost { pub fn approve(self) -> Post { Post { content: self.content, } } } }
Листинг 18-20: PendingReviewPost, создаваемый вызовом request_review на DraftPost, и метод approve, превращающий PendingReviewPost в опубликованный Post
Методы request_review и approve принимают владение self, таким образом потребляя экземпляры DraftPost и PendingReviewPost и преобразуя их в PendingReviewPost и опубликованный Post соответственно. Таким образом, у нас не останется никаких экземпляров DraftPost после вызова request_review на них и так далее. Для структуры PendingReviewPost не определен метод content, поэтому попытка прочитать его содержимое приводит к ошибке компилятора, как и с DraftPost. Поскольку единственный способ получить экземпляр опубликованного Post, у которого определен метод content, - это вызвать метод approve на PendingReviewPost, а единственный способ получить PendingReviewPost - это вызвать метод request_review на DraftPost, мы теперь закодировали рабочий процесс записи блога в систему типов.
Но мы также должны внести некоторые небольшие изменения в main. Методы request_review и approve возвращают новые экземпляры, а не изменяют структуру, для которой они вызываются, поэтому нам нужно добавить больше переназначающих let post = присваиваний для сохранения возвращенных экземпляров. Мы также не можем иметь утверждения о том, что содержимое черновиков и записей на рецензии являются пустыми строками, и нам это не нужно: мы больше не можем компилировать код, который пытается использовать содержимое записей в этих состояниях. Обновленный код в main показан в листинге 18-21.
Файл: src/main.rs
use blog::Post; fn main() { let mut post = Post::new(); post.add_text("I ate a salad for lunch today"); let post = post.request_review(); let post = post.approve(); assert_eq!("I ate a salad for lunch today", post.content()); }
Листинг 18-21: Изменения в main для использования новой реализации рабочего процесса записи блога
Изменения, которые нам нужно было внести в main для переназначения post, означают, что эта реализация больше не совсем следует объектно-ориентированному шаблону состояния: преобразования между состояниями больше не инкапсулированы полностью внутри реализации Post. Однако наша выгода в том, что недопустимые состояния теперь невозможны благодаря системе типов и проверке типов, которая происходит во время компиляции! Это гарантирует, что определенные ошибки, такие как отображение содержимого неопубликованной записи, будут обнаружены до того, как они попадут в рабочую среду.
Попробуйте выполнить задачи, предложенные в начале этого раздела, для крейта blog в том виде, в котором он находится после листинга 18-21, чтобы понять, что вы думаете о дизайне этой версии кода. Обратите внимание, что некоторые задачи могут быть уже выполнены в этом дизайне.
Мы видели, что даже though Rust способен реализовать объектно-ориентированные шаблоны проектирования, другие шаблоны, такие как кодирование состояния в систему типов, также доступны в Rust. Эти шаблоны имеют разные компромиссы. Хотя вы можете быть очень familiar с объектно-ориентированными шаблонами, переосмысление проблемы для использования преимуществ функций Rust может предоставить benefits, такие как предотвращение некоторых ошибок во время компиляции. Объектно-ориентированные шаблоны не всегда будут лучшим решением в Rust из-за определенных функций, таких как владение, которых нет в объектно-ориентированных языках.
Итоги
Независимо от того, считаете ли вы Rust объектно-ориентированным языком после прочтения этой главы, вы теперь знаете, что можете использовать трейт-объекты для получения некоторых объектно-ориентированных функций в Rust. Динамическая диспетчеризация может дать вашему коду некоторую гибкость в обмен на небольшую потерю производительности во время выполнения. Вы можете использовать эту гибкость для реализации объектно-ориентированных шаблонов, которые могут помочь поддерживаемости вашего кода. У Rust также есть другие функции, такие как владение, которых нет в объектно-ориентированных языках. Объектно-ориентированный шаблон не всегда будет лучшим способом использовать сильные стороны Rust, но это доступная опция.
Далее мы рассмотрим шаблоны (patterns), которые являются еще одной особенностью Rust, обеспечивающей большую гибкость. Мы кратко рассматривали их на протяжении всей книги, но еще не видели их полную возможность. Начнем!