Реализация объектно-ориентированного паттерна проектирования

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Реализация объектно-ориентированного паттерна проектирования. Этот практикум является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этом практикуме мы реализуем паттерн состояния в объектно-ориентированном дизайне, чтобы создать структуру записи блога, которая переходит через разные состояния (черновик, на рассмотрении и опубликована) в зависимости от ее поведения, обеспечивая тем самым, что только опубликованные записи блога могут возвращать содержание.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} rust/mutable_variables -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} rust/string_type -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} rust/function_syntax -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} rust/expressions_statements -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} rust/method_syntax -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} rust/traits -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} rust/operator_overloading -.-> lab-100443{{"Реализация объектно-ориентированного паттерна проектирования"}} end

Реализация объектно-ориентированного паттерна проектирования

Паттерн состояния - это объектно-ориентированный паттерн проектирования. Суть паттерна заключается в том, что мы определяем набор состояний, которые может иметь значение внутри. Состояния представляются набором объектов состояния, и поведение значения меняется в зависимости от его состояния. Мы рассмотрим пример структуры записи блога, которая имеет поле для хранения своего состояния, которое будет объектом состояния из набора "черновик", "на рассмотрении" или "опубликована".

Объекты состояния имеют общие функции: в Rust, конечно, мы используем структуры и трейты вместо объектов и наследования. Каждый объект состояния отвечает за свое собственное поведение и за то, когда оно должно измениться на другое состояние. Значение, которое хранит объект состояния, ничего не знает о разном поведении состояний или о том, когда переключаться между состояниями.

Преимуществом использования паттерна состояния является то, что при изменении бизнес-требований программы мы не будем изменять код значения, хранящего состояние, или код, который использует это значение. Мы будем только обновлять код внутри одного из объектов состояния, чтобы изменить его правила или, возможно, добавить больше объектов состояния.

Сначала мы реализуем паттерн состояния более традиционным объектно-ориентированным способом, а затем мы будем использовать подход, который более естественен в Rust. Давайте углубимся и постепенно реализуем рабочий процесс записи блога с использованием паттерна состояния.

Конечная функциональность будет выглядеть так:

  1. Запись блога начинается как пустой черновик.
  2. Когда черновик готов, запрашивается обзор записи.
  3. Когда запись одобряется, она публикуется.
  4. Только опубликованные записи блога возвращают содержание для печати, поэтому неподтвержденные записи не могут случайно быть опубликованы.

Любые другие попытки изменения записи должны быть без эффекта. Например, если мы попытаемся одобрить черновик записи блога до того, как мы запросили обзор, запись должна остаться непубликуемым черновиком.

Листинг 17-11 показывает этот рабочий процесс в кодовом виде: это пример использования API, которое мы реализуем в библиотечном пакете под названием blog. Это еще не скомпилируется, потому что мы не реализовали пакет blog.

Имя файла: src/main.rs

use blog::Post;

fn main() {
  1 let mut post = Post::new();

  2 post.add_text("I ate a salad for lunch today");
  3 assert_eq!("", post.content());

  4 post.request_review();
  5 assert_eq!("", post.content());

  6 post.approve();
  7 assert_eq!("I ate a salad for lunch today", post.content());
}

Листинг 17-11: Код, демонстрирующий ожидаемое поведение, которое мы хотим, чтобы имел пакет blog

Мы хотим, чтобы пользователь мог создавать новый черновик записи блога с помощью Post::new [1]. Мы хотим, чтобы можно было добавлять текст в запись блога [2]. Если мы попытаемся сразу получить содержание записи до одобрения, мы не должны получить никакого текста, потому что запись еще является черновиком. Мы добавили assert_eq! в код для демонстрационных целей [3]. Отличным юнит-тестом для этого было бы утверждение, что черновик записи блога возвращает пустую строку из метода content, но мы не собираемся писать тесты для этого примера.

Далее, мы хотим возможность запросить обзор записи [4], и мы хотим, чтобы content возвращал пустую строку в течение ожидания обзора [5]. Когда запись получает одобрение [6], она должна быть опубликована, что означает, что текст записи будет возвращен, когда вызывается content [7].

Заметим, что единственный тип, с которым мы взаимодействуем из пакета, это тип Post. Этот тип будет использовать паттерн состояния и будет хранить значение, которое будет одним из трех объектов состояния, представляющих различные состояния, в которых может быть запись - черновик, на рассмотрении или опубликована. Переключение между состояниями будет управляться внутри типа Post. Состояния меняются в ответ на методы, вызываемые пользователями нашей библиотеки на экземпляре Post, но они не должны напрямую управлять изменением состояния. Кроме того, пользователи не могут ошибиться с состояниями, например, опубликовать запись до ее обзора.

Определение Post и создание нового экземпляра в состоянии черновика

Давайте приступим к реализации библиотеки! Мы знаем, что нам нужна общедоступная структура Post, которая будет хранить некоторое содержание, поэтому мы начнем с определения структуры и связанной с ней общедоступной функции new, чтобы создать экземпляр Post, как показано в Листинге 17-12. Мы также создадим приватный трейт State, который определит поведение, которое должны иметь все объекты состояния для Post.

Затем Post будет хранить объект трейта Box<dyn State> внутри Option<T> в приватном поле с именем state, чтобы хранить объект состояния. Через некоторое время вы поймете, почему необходимо Option<T>.

Имя файла: src/lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
          1 state: Some(Box::new(Draft {})),
          2 content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Листинг 17-12: Определение структуры Post и функции new, которая создает новый экземпляр Post, трейта State и структуры Draft

Трейт State определяет поведение, общие для различных состояний записи. Объекты состояния - это Draft, PendingReview и Published, и все они будут реализовывать трейт State. На данный момент трейт не имеет никаких методов, и мы начнем с определения только состояния Draft, потому что именно в этом состоянии должна начинаться запись.

Когда мы создаем новый экземпляр Post, мы устанавливаем его поле state в значение Some, которое содержит Box [1]. Этот Box ссылается на новый экземпляр структуры Draft. Это гарантирует, что каждый раз, когда мы создаем новый экземпляр Post, он будет начинаться как черновик. Поскольку поле state структуры Post приватно, невозможно создать Post в каком-либо другом состоянии! В функции Post::new мы устанавливаем поле content в новую, пустую String [2].

Сохранение текста содержания записи блога

Мы видели в Листинге 17-11, что мы хотим иметь возможность вызвать метод с именем add_text и передать ему &str, который затем добавляется в качестве текстового содержания записи блога. Мы реализуем это в виде метода, а не экспортируем поле content как pub, чтобы позже мы могли реализовать метод, который будет контролировать, как данные поля content читаются. Метод add_text довольно простой, поэтому давайте добавим реализацию в Листинге 17-13 в блок impl Post.

Имя файла: src/lib.rs

impl Post {
    --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Листинг 17-13: Реализация метода add_text для добавления текста в content записи

Метод add_text принимает изменяемую ссылку на self, потому что мы изменяем экземпляр Post, на котором вызываем add_text. Затем мы вызываем push_str на String в content и передаем аргумент text, чтобы добавить его в сохраненное content. Это поведение не зависит от состояния записи, поэтому оно не является частью паттерна состояния. Метод add_text вовсе не взаимодействует с полем state, но он является частью поведения, которое мы хотим поддерживать.

Гарантия пустого содержания черновика записи блога

Даже после того, как мы вызовем add_text и добавим некоторое содержание в нашу запись, мы по-прежнему хотим, чтобы метод content возвращал пустой срез строки, потому что запись еще находится в состоянии черновика, как показано в [3] в Листинге 17-11. В данный момент давайте реализуем метод content самым простым способом, который будет удовлетворять этому требованию: всегда возвращать пустой срез строки. Мы изменим это позже, когда реализуем возможность изменять состояние записи, чтобы ее можно было опубликовать. До сих пор записи могут находиться только в состоянии черновика, поэтому содержание записи всегда должно быть пустым. Листинг 17-14 показывает эту временную реализацию.

Имя файла: src/lib.rs

impl Post {
    --snip--
    pub fn content(&self) -> &str {
        ""
    }
}

Листинг 17-14: Добавление временной реализации для метода content на Post, который всегда возвращает пустой срез строки

С добавленным методом content все в Листинге 17-11 до строки [3] работает как ожидается.

Запрос на обзор изменяет состояние записи

Далее, нам нужно добавить функциональность для запроса обзора записи, которая должна изменить ее состояние с Draft на PendingReview. Листинг 17-15 показывает этот код.

Имя файла: src/lib.rs

impl Post {
    --snip--
  1 pub fn request_review(&mut self) {
      2 if let Some(s) = self.state.take() {
          3 self.state = Some(s.request_review())
        }
    }
}

trait State {
  4 fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      5 Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
      6 self
    }
}

Листинг 17-15: Реализация методов request_review на Post и трейте State

Мы даем Post публичный метод с именем request_review, который будет принимать изменяемую ссылку на self [1]. Затем мы вызываем внутренний метод request_review для текущего состояния Post [3], и этот второй метод request_review потребляет текущее состояние и возвращает новое состояние.

Мы добавляем метод request_review в трейт State [4]; все типы, которые реализуют этот трейт, теперь должны реализовать метод request_review. Обратите внимание, что вместо self, &self или &mut self в качестве первого параметра метода у нас self: Box<Self>. Эта синтаксис означает, что метод действителен только при вызове на Box, хранящем этот тип. Эта синтаксис забирает владение Box<Self>, делая старое состояние недействительным, чтобы значение состояния Post могло преобразоваться в новое состояние.

Чтобы потребить старое состояние, метод request_review должен забрать владение значением состояния. Именно здесь приходит Option в поле state структуры Post: мы вызываем метод take, чтобы вытащить значение Some из поля state и оставить None на его месте, потому что Rust не позволяет иметь незаполненные поля в структурах [2]. Это позволяет нам переместить значение state из Post вместо того, чтобы брать его в долг. Затем мы установим значение state записи равным результату этой операции.

Мы должны временно установить state в None, а не напрямую с помощью кода, такого как self.state = self.state.request_review();, чтобы получить владение значением state. Это гарантирует, что Post не сможет использовать старое значение state после того, как мы преобразовали его в новое состояние.

Метод request_review на Draft возвращает новый, заboxed экземпляр новой структуры PendingReview, которая представляет состояние, когда запись ожидает обзора [5]. Структура PendingReview также реализует метод request_review, но не делает никаких преобразований. Вместо этого она возвращает себя [6], потому что когда мы запрашиваем обзор записи, которая уже находится в состоянии PendingReview, она должна оставаться в этом состоянии.

Теперь мы можем увидеть преимущества паттерна состояния: метод request_review на Post одинаковый независимо от значения его state. Каждое состояние отвечает за свои собственные правила.

Мы оставим метод content на Post без изменений, возвращающий пустой срез строки. Теперь мы можем иметь Post в состоянии PendingReview так же, как и в состоянии Draft, но мы хотим иметь одинаковое поведение в состоянии PendingReview. Листинг 17-11 теперь работает до строки [5]!

Добавление 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-выражение или аналогичное для обработки каждого возможного варианта. Это может быть более повторяющимся, чем этот вариант с объектами трейтов.

Сравнительные преимущества и недостатки паттерна состояния

Мы показали, что 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. Однако это не сработает: когда используем State в качестве объекта трейта, трейт не знает, какой будет конкретный self точно, поэтому тип возврата неизвестен на этапе компиляции.

Другая дублировка включает в себя похожие реализации методов request_review и approve на Post. Оба метода делегируют реализации того же метода на значении в поле state структуры Option и устанавливают новое значение поля state равным результату. Если бы у нас было много методов на Post, которые следовали этой схеме, мы могли бы рассмотреть определение макроса для устранения повторений (см. "Макросы").

Реализуя паттерн состояния так, как он определен для объектно-ориентированных языков, мы не充分利用 Rust 的优势。让我们看看我们可以对blog crate 进行哪些更改,以使无效状态和转换成为编译时错误。

Кодирование состояний и поведения в виде типов

Мы покажем, как пересмотреть паттерн состояния, чтобы получить другой набор сравнительных преимуществ и недостатков. Вместо полной инкапсуляции состояний и переходов, чтобы внешний код не имел о них никакой информации, мы будем кодировать состояния в разные типы. В результате система типизации Rust будет предотвращать попытки использовать черновики записей там, где разрешены только опубликованные записи, выдавая ошибку компиляции.

Рассмотрим первую часть main в Листинге 17-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. Таким образом, если мы попытаемся получить содержание черновика записи, мы получим ошибку компиляции, которая сообщит нам, что метод не существует. В результате мы не сможем случайно показать содержание черновика записи в production, потому что этот код даже не скомпилируется. Листинг 17-19 показывает определение структуры Post и структуры DraftPost, а также методы для каждой из них.

Имя файла: src/lib.rs

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
  1 pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

  2 pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
  3 pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Листинг 17-19: Post с методом content и DraftPost без метода content

И структура Post, и структура DraftPost имеют приватное поле content, которое хранит текст записи блога. Структуры больше не имеют поля state, потому что мы переносим кодирование состояния в типы структур. Структура Post будет представлять опубликованную запись, и у нее есть метод content, который возвращает content [2].

У нас по-прежнему есть функция Post::new, но вместо возврата экземпляра Post она возвращает экземпляр DraftPost [1]. Поскольку content приватно и нет функций, которые возвращают Post, невозможно создать экземпляр Post в настоящее время.

Структура DraftPost имеет метод add_text, поэтому мы можем добавлять текст в content как и раньше [3], но обратите внимание, что для DraftPost не определен метод content! Теперь программа гарантирует, что все записи начинаются как черновики, и содержание черновиков недоступно для отображения. Любая попытка обойти эти ограничения приведет к ошибке компиляции.

Реализация переходов в виде преобразований в разные типы

Итак, как мы получаем опубликованную запись? Мы хотим закрепить правило, согласно которому черновик записи должен быть обзарен и одобрен, прежде чем можно опубликовать его. Запись в состоянии ожидания обзора по-прежнему не должна отображать никакого содержания. Реализуем эти ограничения, добавив еще одну структуру, PendingReviewPost, определив метод request_review на DraftPost, чтобы он возвращал PendingReviewPost, и определив метод approve на PendingReviewPost, чтобы он возвращал Post, как показано в Листинге 17-20.

Имя файла: src/lib.rs

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,
        }
    }
}

Листинг 17-20: PendingReviewPost, который создается при вызове request_review на DraftPost, и метод approve, который превращает PendingReviewPost в опубликованную Post

Методы request_review и approve берут владение self, тем самым потребляя экземпляры DraftPost и PendingReviewPost и преобразуя их соответственно в PendingReviewPost и опубликованную Post. Таким образом, после вызова request_review на них у нас не останется никаких экземпляров DraftPost и т.д. Структуре PendingReviewPost не определен метод content, поэтому попытка прочитать ее содержание приводит к ошибке компиляции, как и в случае с DraftPost. Поскольку единственный способ получить экземпляр опубликованной Post с определенным методом content - это вызвать метод approve на PendingReviewPost, а единственный способ получить PendingReviewPost - это вызвать метод request_review на DraftPost, мы теперь закодировали рабочий процесс записи блога в систему типов.

Но мы также должны внести некоторые небольшие изменения в main. Методы request_review и approve возвращают новые экземпляры, а не модифицируют структуру, на которой они вызываются, поэтому нам нужно добавить больше инструкций let post = для создания новых переменных, чтобы сохранить возвращаемые экземпляры. Также мы не можем утверждать, что содержание черновика и записи в состоянии ожидания обзора - это пустые строки, и мы не нуждаемся в этом: мы не можем скомпилировать код, который пытается использовать содержание записей в этих состояниях. Обновленный код в main показан в Листинге 17-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());
}

Листинг 17-21: Изменения в main для использования нового реализации рабочего процесса записи блога

Изменения, которые мы внесли в main, чтобы переопределить post, означают, что эта реализация уже не совсем соответствует объекно-ориентированному паттерну состояния: преобразования между состояниями больше не полностью инкапсулированы внутри реализации Post. Однако наша выгода заключается в том, что теперь невозможны недействительные состояния из-за системы типов и проверки типов, которая происходит на этапе компиляции! Это гарантирует, что некоторые ошибки, такие как отображение содержания неопубликованной записи, будут обнаружены до того, как код перейдет в production.

Попробуйте выполнить задачи, предложенные в начале этого раздела, для blog crate в том виде, в котором он представлен после Листинга 17-21, чтобы понять, как вы относитесь к дизайну этой версии кода. Обратите внимание, что некоторые из задач могут быть уже выполнены в этом дизайне.

Мы убедились, что хотя Rust способен реализовать объектно-ориентированные паттерны проектирования, в Rust также доступны и другие паттерны, такие как кодирование состояния в систему типов. Эти паттерны имеют разные сравнительные преимущества и недостатки. Хотя вы, возможно, знакомы с объектно-ориентированными паттернами, пересмотр проблемы с использованием особенностей Rust может принести выгоду, например, предотвращение некоторых ошибок на этапе компиляции. Объектно-ориентированные паттерны не всегда будут наилучшим решением в Rust из-за некоторых особенностей, таких как владение ресурсами, которых не имеют объектно-ориентированные языки.

Резюме

Поздравляем! Вы завершили лабораторную работу по реализации объектно-ориентированного паттерна проектирования. Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.