Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

实现面向对象设计模式

Implementing an Object-Oriented Design Pattern

状态模式(state pattern)是一种面向对象的设计模式。该模式的核心在于,我们定义了一个值在内部可以拥有的状态集合。状态由一组 状态对象(state objects)表示,值的行为根据其状态而改变。我们将通过一个博客文章(blog post)结构体的示例来展开,该结构体有一个字段用于持有其状态,状态将是“草稿”(draft)、“审核”(review)或“已发布”(published)状态对象集合中的一个。

The state pattern is an object-oriented design pattern. The crux of the pattern is that we define a set of states a value can have internally. The states are represented by a set of state objects, and the value’s behavior changes based on its state. We’re going to work through an example of a blog post struct that has a field to hold its state, which will be a state object from the set “draft,” “review,” or “published.”

状态对象共享功能:当然,在 Rust 中,我们使用结构体和 trait 而不是对象和继承。每个状态对象负责其自身的行为,并控制何时应转换为另一个状态。持有状态对象的值对状态的不同行为或何时进行状态转换一无所知。

The state objects share functionality: In Rust, of course, we use structs and traits rather than objects and inheritance. Each state object is responsible for its own behavior and for governing when it should change into another state. The value that holds a state object knows nothing about the different behavior of the states or when to transition between states.

使用状态模式的优势在于,当程序的业务需求发生变化时,我们不需要更改持有状态的值的代码或使用该值的代码。我们只需要更新其中一个状态对象内部的代码来更改其规则,或者可能添加更多的状态对象。

The advantage of using the state pattern is that, when the business requirements of the program change, we won’t need to change the code of the value holding the state or the code that uses the value. We’ll only need to update the code inside one of the state objects to change its rules or perhaps add more state objects.

首先,我们将以一种更传统的面向对象方式来实现状态模式。然后,我们将使用一种在 Rust 中更为自然的方法。让我们开始逐步使用状态模式来实现一个博客文章工作流。

First, we’re going to implement the state pattern in a more traditional object-oriented way. Then, we’ll use an approach that’s a bit more natural in Rust. Let’s dig in to incrementally implement a blog post workflow using the state pattern.

最终的功能将如下所示:

The final functionality will look like this:

  1. 博客文章以空草稿开始。

  2. 当草稿完成时,请求对该文章进行审核。

  3. 当文章被批准后,它将被发布。

  4. 只有已发布的博客文章才会返回打印内容,以免未经批准的文章被意外发布。

  5. A blog post starts as an empty draft.

  6. When the draft is done, a review of the post is requested.

  7. When the post is approved, it gets published.

  8. Only published blog posts return content to print so that unapproved posts can’t accidentally be published.

对文章尝试进行的任何其他更改都不应产生任何影响。例如,如果我们试图在请求审核之前批准一篇草稿博客文章,该文章应保持为未发布的草稿。

Any other changes attempted on a post should have no effect. For example, if we try to approve a draft blog post before we’ve requested a review, the post should remain an unpublished draft.

尝试传统的面向对象风格

Attempting Traditional Object-Oriented Style

解决同一个问题的方法有无数种,每种方法都有不同的权衡。本节的实现更多地采用传统的面向对象风格,这在 Rust 中是可以编写的,但没有利用 Rust 的一些优势。稍后,我们将演示另一种解决方案,它仍然使用面向对象设计模式,但结构对于有面向对象经验的程序员来说可能看起来不那么熟悉。我们将比较这两种解决方案,以体验以不同于其他语言的方式设计 Rust 代码所带来的权衡。

There are infinite ways to structure code to solve the same problem, each with different trade-offs. This section’s implementation is more of a traditional object-oriented style, which is possible to write in Rust, but doesn’t take advantage of some of Rust’s strengths. Later, we’ll demonstrate a different solution that still uses the object-oriented design pattern but is structured in a way that might look less familiar to programmers with object-oriented experience. We’ll compare the two solutions to experience the trade-offs of designing Rust code differently than code in other languages.

示例 18-11 以代码形式展示了这一工作流:这是我们将在名为 blog 的库 crate 中实现的 API 的使用示例。这目前还无法编译,因为我们还没有实现 blog crate。

Listing 18-11 shows this workflow in code form: This is an example usage of the API we’ll implement in a library crate named blog. This won’t compile yet because we haven’t implemented the blog crate.

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());
}

我们希望允许用户使用 Post::new 创建一个新的草稿博客文章。我们希望允许向博客文章添加文本。如果我们在批准之前立即尝试获取文章内容,我们不应该得到任何文本,因为该文章仍是草稿。为了演示目的,我们在代码中添加了 assert_eq!。对此进行单元测试的一个极佳方法是断言草稿博客文章从 content 方法返回一个空字符串,但我们不打算为本示例编写测试。

We want to allow the user to create a new draft blog post with Post::new. We want to allow text to be added to the blog post. If we try to get the post’s content immediately, before approval, we shouldn’t get any text because the post is still a draft. We’ve added assert_eq! in the code for demonstration purposes. An excellent unit test for this would be to assert that a draft blog post returns an empty string from the content method, but we’re not going to write tests for this example.

接下来,我们希望能够请求对文章进行审核,并希望在等待审核期间 content 返回一个空字符串。当文章获得批准后,它应该被发布,这意味着当调用 content 时,将返回文章的文本。

Next, we want to enable a request for a review of the post, and we want content to return an empty string while waiting for the review. When the post receives approval, it should get published, meaning the text of the post will be returned when content is called.

请注意,我们从 crate 中交互的唯一类型是 Post 类型。该类型将使用状态模式,并持有一个值,该值将是代表文章可能处于的三种状态(草稿、审核中或已发布)之一的状态对象。从一个状态到另一个状态的更改将在 Post 类型内部进行管理。状态会根据库用户在 Post 实例上调用的方法而改变,但用户不必直接管理状态变更。此外,用户不会在状态上犯错,例如在审核之前发布文章。

Notice that the only type we’re interacting with from the crate is the Post type. This type will use the state pattern and will hold a value that will be one of three state objects representing the various states a post can be in—draft, review, or published. Changing from one state to another will be managed internally within the Post type. The states change in response to the methods called by our library’s users on the Post instance, but they don’t have to manage the state changes directly. Also, users can’t make a mistake with the states, such as publishing a post before it’s reviewed.

定义 Post 并创建新实例

Defining Post and Creating a New Instance

让我们开始实现该库!我们知道我们需要一个持有某些内容的公共 Post 结构体,因此我们将从结构体定义和用于创建 Post 实例的相关公共 new 函数开始,如示例 18-12 所示。我们还将创建一个私有的 State trait,它将定义 Post 的所有状态对象必须具备的行为。

Let’s get started on the implementation of the library! We know we need a public Post struct that holds some content, so we’ll start with the definition of the struct and an associated public new function to create an instance of Post, as shown in Listing 18-12. We’ll also make a private State trait that will define the behavior that all state objects for a Post must have.

然后,Post 将在私有字段 state 中持有一个包装在 Option<T> 里的 Box<dyn State> trait 对象,以持有状态对象。稍后你就会看到为什么 Option<T> 是必要的。

Then, Post will hold a trait object of Box<dyn State> inside an Option<T> in a private field named state to hold the state object. You’ll see why the Option<T> is necessary in a bit.

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

State trait 定义了不同文章状态共享的行为。状态对象包括 DraftPendingReviewPublished,它们都将实现 State trait。目前,该 trait 没有任何方法,我们将从仅定义 Draft 状态开始,因为那是我们希望文章开始时的状态。

The State trait defines the behavior shared by different post states. The state objects are Draft, PendingReview, and Published, and they will all implement the State trait. For now, the trait doesn’t have any methods, and we’ll start by defining just the Draft state because that is the state we want a post to start in.

当我们创建一个新的 Post 时,我们将它的 state 字段设置为一个持有一个 BoxSome 值。这个 Box 指向 Draft 结构体的一个新实例。这确保了每当我们创建一个新的 Post 实例时,它都会以草稿形式开始。由于 Poststate 字段是私有的,因此无法以任何其他状态创建 Post!在 Post::new 函数中,我们将 content 字段设置为一个新的空 String

When we create a new Post, we set its state field to a Some value that holds a Box. This Box points to a new instance of the Draft struct. This ensures that whenever we create a new instance of Post, it will start out as a draft. Because the state field of Post is private, there is no way to create a Post in any other state! In the Post::new function, we set the content field to a new, empty String.

存储文章内容的文本

Storing the Text of the Post Content

我们在示例 18-11 中看到,我们希望能够调用一个名为 add_text 的方法并传递给它一个 &str,该字符串随后作为博客文章的文本内容被添加。我们将其实现为一个方法,而不是将 content 字段公开为 pub,这样稍后我们可以实现一个控制如何读取 content 字段数据的方法。add_text 方法非常简单,所以让我们在示例 18-13 中将实现添加到 impl Post 块中。

We saw in Listing 18-11 that we want to be able to call a method named add_text and pass it a &str that is then added as the text content of the blog post. We implement this as a method, rather than exposing the content field as pub, so that later we can implement a method that will control how the content field’s data is read. The add_text method is pretty straightforward, so let’s add the implementation in Listing 18-13 to the impl Post block.

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

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

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

trait State {}

struct Draft {}

impl State for Draft {}

add_text 方法接收一个对 self 的可变引用,因为我们正在更改调用 add_textPost 实例。然后我们对 content 中的 String 调用 push_str,并传入 text 参数以添加到保存的 content 中。这种行为不依赖于文章所处的状态,因此它不是状态模式的一部分。add_text 方法根本不与 state 字段交互,但它是我们想要支持的行为的一部分。

The add_text method takes a mutable reference to self because we’re changing the Post instance that we’re calling add_text on. We then call push_str on the String in content and pass the text argument to add to the saved content. This behavior doesn’t depend on the state the post is in, so it’s not part of the state pattern. The add_text method doesn’t interact with the state field at all, but it is part of the behavior we want to support.

确保草稿文章的内容为空

Ensuring That the Content of a Draft Post Is Empty

即使在我们调用了 add_text 并为文章添加了一些内容之后,我们仍然希望 content 方法返回一个空字符串切片,因为文章仍处于草稿状态,如示例 18-11 中的第一个 assert_eq! 所示。目前,让我们用能满足这一要求的、最简单的方法来实现 content 方法:始终返回一个空字符串切片。一旦我们实现了更改文章状态以便它可以发布的功能,我们稍后将更改此方法。到目前为止,文章只能处于草稿状态,因此文章内容应始终为空。示例 18-14 展示了这一占位符实现。

Even after we’ve called add_text and added some content to our post, we still want the content method to return an empty string slice because the post is still in the draft state, as shown by the first assert_eq! in Listing 18-11. For now, let’s implement the content method with the simplest thing that will fulfill this requirement: always returning an empty string slice. We’ll change this later once we implement the ability to change a post’s state so that it can be published. So far, posts can only be in the draft state, so the post content should always be empty. Listing 18-14 shows this placeholder implementation.

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

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

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

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

trait State {}

struct Draft {}

impl State for Draft {}

有了这个添加的 content 方法,示例 18-11 中直到第一个 assert_eq! 的所有内容都能按预期工作。

With this added content method, everything in Listing 18-11 through the first assert_eq! works as intended.

请求审核,这会改变文章的状态

Requesting a Review, Which Changes the Post’s State

接下来,我们需要添加请求审核文章的功能,这应该将其状态从 Draft 更改为 PendingReview。示例 18-15 展示了这段代码。

Next, we need to add functionality to request a review of a post, which should change its state from Draft to PendingReview. Listing 18-15 shows this code.

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

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

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

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

    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 的可变引用。然后,我们在 Post 的当前状态上调用一个内部的 request_review 方法,而这第二个 request_review 方法会消耗当前状态并返回一个新状态。

We give Post a public method named request_review that will take a mutable reference to self. Then, we call an internal request_review method on the current state of Post, and this second request_review method consumes the current state and returns a new state.

我们将 request_review 方法添加到 State trait 中;所有实现该 trait 的类型现在都需要实现 request_review 方法。请注意,该方法的第一个参数不是 self&self&mut self,而是 self: Box<Self>。这种语法意味着该方法仅在对持有该类型的 Box 调用时才有效。这种语法获取了 Box<Self> 的所有权,使旧状态失效,以便 Post 的状态值可以转换为新状态。

We add the request_review method to the State trait; all types that implement the trait will now need to implement the request_review method. Note that rather than having self, &self, or &mut self as the first parameter of the method, we have self: Box<Self>. This syntax means the method is only valid when called on a Box holding the type. This syntax takes ownership of Box<Self>, invalidating the old state so that the state value of the Post can transform into a new state.

为了消耗旧状态,request_review 方法需要获取状态值的所有权。这就是 Poststate 字段中 Option 的用武之地:我们调用 take 方法将 Some 值从 state 字段中取出,并在其位置留下一个 None,因为 Rust 不允许我们在结构体中留有未填充的字段。这让我们可以将 state 值从 Post 中移出而不是借用它。然后,我们将文章的 state 值设置为该操作的结果。

To consume the old state, the request_review method needs to take ownership of the state value. This is where the Option in the state field of Post comes in: We call the take method to take the Some value out of the state field and leave a None in its place because Rust doesn’t let us have unpopulated fields in structs. This lets us move the state value out of Post rather than borrowing it. Then, we’ll set the post’s state value to the result of this operation.

我们需要暂时将 state 设置为 None,而不是直接通过 self.state = self.state.request_review(); 这样的代码来设置它,以便获得 state 值的所有权。这确保了在我们将其转换为新状态后,Post 无法再使用旧的 state 值。

We need to set state to None temporarily rather than setting it directly with code like self.state = self.state.request_review(); to get ownership of the state value. This ensures that Post can’t use the old state value after we’ve transformed it into a new state.

Draft 上的 request_review 方法返回一个新的、装箱的 PendingReview 结构体实例,该结构体代表文章正在等待审核的状态。PendingReview 结构体也实现了 request_review 方法,但不进行任何转换。相反,它返回它自己,因为当我们对已经处于 PendingReview 状态的文章请求审核时,它应该保持在 PendingReview 状态。

The request_review method on Draft returns a new, boxed instance of a new PendingReview struct, which represents the state when a post is waiting for a review. The PendingReview struct also implements the request_review method but doesn’t do any transformations. Rather, it returns itself because when we request a review on a post already in the PendingReview state, it should stay in the PendingReview state.

现在我们可以开始看到状态模式的优势了:无论其 state 值如何,Post 上的 request_review 方法都是一样的。每个状态负责其自身的规则。

Now we can start seeing the advantages of the state pattern: The request_review method on Post is the same no matter its state value. Each state is responsible for its own behavior.

我们将保持 Post 上的 content 方法不变,返回一个空字符串切片。我们现在可以拥有处于 PendingReview 状态以及 Draft 状态的 Post,但我们希望在 PendingReview 状态下具有相同的行为。示例 18-11 现在可以运行到第二个 assert_eq! 调用了!

We’ll leave the content method on Post as is, returning an empty string slice. We can now have a Post in the PendingReview state as well as in the Draft state, but we want the same behavior in the PendingReview state. Listing 18-11 now works up to the second assert_eq! call!

添加 approve 以改变 content 的行为

Adding approve to Change content’s Behavior

approve 方法将类似于 request_review 方法:它将 state 设置为当前状态认为在获得批准时应具有的值,如示例 18-16 所示。

The approve method will be similar to the request_review method: It will set state to the value that the current state says it should have when that state is approved, as shown in Listing 18-16.

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

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

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

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

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    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 request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

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

    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 trait,并添加一个实现 State 的新结构体,即 Published 状态。

We add the approve method to the State trait and add a new struct that implements State, the Published state.

类似于 PendingReview 上的 request_review 的工作方式,如果我们对 Draft 调用 approve 方法,它将没有效果,因为 approve 将返回 self。当我们对 PendingReview 调用 approve 时,它返回一个新的、装箱的 Published 结构体实例。Published 结构体实现了 State trait,对于 request_review 方法和 approve 方法,它都返回其自身,因为在这些情况下文章应该保持在 Published 状态。

Similar to the way request_review on PendingReview works, if we call the approve method on a Draft, it will have no effect because approve will return self. When we call approve on PendingReview, it returns a new, boxed instance of the Published struct. The Published struct implements the State trait, and for both the request_review method and the approve method, it returns itself because the post should stay in the Published state in those cases.

现在我们需要更新 Post 上的 content 方法。我们希望从 content 返回的值取决于 Post 的当前状态,因此我们将让 Post 委托给其 state 上定义的 content 方法,如示例 18-17 所示。

Now we need to update the content method on Post. We want the value returned from content to depend on the current state of the Post, so we’re going to have the Post delegate to a content method defined on its state, as shown in Listing 18-17.

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

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

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

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    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 {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

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

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

因为目标是将所有这些规则保持在实现 State 的结构体内部,所以我们在 state 中的值上调用 content 方法,并传入文章实例(即 self)作为参数。然后,我们返回对 state 值使用 content 方法后返回的值。

Because the goal is to keep all of these rules inside the structs that implement State, we call a content method on the value in state and pass the post instance (that is, self) as an argument. Then, we return the value that’s returned from using the content method on the state value.

我们对 Option 调用 as_ref 方法,因为我们想要对 Option 内部值的引用而不是其所有权。因为 state 是一个 Option<Box<dyn State>>,当我们调用 as_ref 时,会返回一个 Option<&Box<dyn State>>。如果我们不调用 as_ref,我们会得到一个错误,因为我们无法从函数参数借用的 &self 中移出 state

We call the as_ref method on the Option because we want a reference to the value inside the Option rather than ownership of the value. Because state is an Option<Box<dyn State>>, when we call as_ref, an Option<&Box<dyn State>> is returned. If we didn’t call as_ref, we would get an error because we can’t move state out of the borrowed &self of the function parameter.

然后我们调用 unwrap 方法,我们知道它永远不会发生 panic,因为我们知道 Post 上的方法确保了当这些方法完成时 state 始终包含一个 Some 值。这是我们在第 9 章 “当你拥有比编译器更多的信息时” 一节中讨论过的情况之一:当我们知道 None 值永远不可能出现时,即使编译器无法理解这一点。

We then call the unwrap method, which we know will never panic because we know the methods on Post ensure that state will always contain a Some value when those methods are done. This is one of the cases we talked about in the “When You Have More Information Than the Compiler” section of Chapter 9 when we know that a None value is never possible, even though the compiler isn’t able to understand that.

此时,当我们对 &Box<dyn State> 调用 content 时,解引用强制转换(deref coercion)将对 &Box 起作用,因此 content 方法最终将在实现 State trait 的类型上被调用。这意味着我们需要将 content 添加到 State trait 定义中,而这正是我们将根据我们拥有的状态放入返回什么内容的逻辑的地方,如示例 18-18 所示。

At this point, when we call content on the &Box<dyn State>, deref coercion will take effect on the & and the Box so that the content method will ultimately be called on the type that implements the State trait. That means we need to add content to the State trait definition, and that is where we’ll put the logic for what content to return depending on which state we have, as shown in Listing 18-18.

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

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

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--

struct Draft {}

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

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

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

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

我们为 content 方法添加了一个返回空字符串切片的默认实现。这意味着我们不需要在 DraftPendingReview 结构体上实现 contentPublished 结构体将重写 content 方法并返回 post.content 中的值。虽然方便,但在 State 上由 content 方法决定 Post 的内容,模糊了 State 的责任与 Post 的责任之间的界限。

We add a default implementation for the content method that returns an empty string slice. That means we don’t need to implement content on the Draft and PendingReview structs. The Published struct will override the content method and return the value in post.content. While convenient, having the content method on State determine the content of the Post is blurring the lines between the responsibility of State and the responsibility of Post.

请注意,我们需要在这个方法上使用生命周期注解,正如我们在第 10 章中所讨论的。我们将对 post 的引用作为参数,并返回对该 post 一部分的引用,因此返回引用的生命周期与 post 参数的生命周期相关。

Note that we need lifetime annotations on this method, as we discussed in Chapter 10. We’re taking a reference to a post as an argument and returning a reference to part of that post, so the lifetime of the returned reference is related to the lifetime of the post argument.

大功告成——示例 18-11 的所有内容现在都能工作了!我们已经按照博客文章工作流的规则实现了状态模式。与规则相关的逻辑存在于状态对象中,而不是散布在 Post 中。

And we’re done—all of Listing 18-11 now works! We’ve implemented the state pattern with the rules of the blog post workflow. The logic related to the rules lives in the state objects rather than being scattered throughout Post.

为什么不用枚举?

Why Not An Enum?

你可能一直在想,为什么我们不使用一个带有不同可能文章状态作为变体的枚举。这当然是一个可能的解决方案;试一试并比较最终结果,看看你更喜欢哪一个!使用枚举的一个缺点是,检查枚举值的每个地方都需要一个 match 表达式或类似结构来处理每个可能的变体。这可能比这个 trait 对象解决方案更重复。

You may have been wondering why we didn’t use an enum with the different possible post states as variants. That’s certainly a possible solution; try it and compare the end results to see which you prefer! One disadvantage of using an enum is that every place that checks the value of the enum will need a match expression or similar to handle every possible variant. This could get more repetitive than this trait object solution.

评估状态模式

Evaluating the State Pattern

我们已经展示了 Rust 能够实现面向对象的状态模式,以封装文章在每个状态下应具有的不同行为。 Post 上的方法对各种行为一无所知。由于我们组织代码的方式,我们只需看一个地方就能知道已发布文章的各种行为方式: Published 结构体上 State trait 的实现。

We’ve shown that Rust is capable of implementing the object-oriented state pattern to encapsulate the different kinds of behavior a post should have in each state. The methods on Post know nothing about the various behaviors. Because of the way we organized the code, we have to look in only one place to know the different ways a published post can behave: the implementation of the State trait on the Published struct.

如果我们要创建一个不使用状态模式的替代实现,我们可能会在 Post 的方法中,甚至在检查文章状态并据此改变行为的 main 代码中使用 match 表达式。那意味着我们将不得不看好几个地方才能理解文章处于已发布状态的所有影响。

If we were to create an alternative implementation that didn’t use the state pattern, we might instead use match expressions in the methods on Post or even in the main code that checks the state of the post and changes behavior in those places. That would mean we would have to look in several places to understand all the implications of a post being in the published state.

使用状态模式, Post 方法和我们使用 Post 的地方不需要 match 表达式,而且要添加一个新状态,我们只需要在一个位置添加一个新结构体并在该结构体上实现 trait 方法即可。

With the state pattern, the Post methods and the places we use Post don’t need match expressions, and to add a new state, we would only need to add a new struct and implement the trait methods on that one struct in one location.

使用状态模式的实现很容易扩展以添加更多功能。要体验维护使用状态模式的代码的简单性,请尝试以下几个建议:

The implementation using the state pattern is easy to extend to add more functionality. To see the simplicity of maintaining code that uses the state pattern, try a few of these suggestions:

  • 添加一个 reject 方法,将文章的状态从 PendingReview 改回 Draft

  • 要求调用两次 approve 才能将状态更改为 Published

  • 仅允许用户在文章处于 Draft 状态时添加文本内容。提示:让状态对象负责内容可能发生的变化,但不负责修改 Post

  • Add a reject method that changes the post’s state from PendingReview back to Draft.

  • Require two calls to approve before the state can be changed to Published.

  • Allow users to add text content only when a post is in the Draft state. Hint: have the state object responsible for what might change about the content but not responsible for modifying the Post.

状态模式的一个缺点是,由于各状态实现了状态间的转换,一些状态彼此耦合。如果我们要在 PendingReviewPublished 之间添加另一个状态,例如 Scheduled,我们将不得不更改 PendingReview 中的代码以转而转换为 Scheduled。如果 PendingReview 不需要随着新状态的添加而改变,那工作量会更少,但那意味着需要切换到另一种设计模式。

One downside of the state pattern is that, because the states implement the transitions between states, some of the states are coupled to each other. If we add another state between PendingReview and Published, such as Scheduled, we would have to change the code in PendingReview to transition to Scheduled instead. It would be less work if PendingReview didn’t need to change with the addition of a new state, but that would mean switching to another design pattern.

另一个缺点是我们将一些逻辑重复了。为了消除一些重复,我们可能会尝试在 State trait 上为返回 selfrequest_reviewapprove 方法编写默认实现。然而,这行不通:当将 State 用作 trait 对象时,trait 并不确切知道具体的 self 会是什么,因此返回类型在编译时是未知的。(这是前面提到的 dyn 兼容性规则之一。)

Another downside is that we’ve duplicated some logic. To eliminate some of the duplication, we might try to make default implementations for the request_review and approve methods on the State trait that return self. However, this wouldn’t work: When using State as a trait object, the trait doesn’t know what the concrete self will be exactly, so the return type isn’t known at compile time. (This is one of the dyn compatibility rules mentioned earlier.)

其他重复包括 Postrequest_reviewapprove 方法的类似实现。这两个方法都对 Poststate 字段使用 Option::take,如果 stateSome,它们委托给包装值对同一方法的实现,并将 state 字段的新值设置为结果。如果我们在 Post 上有很多遵循这种模式的方法,我们可能会考虑定义一个宏来消除这种重复(见第 20 章 “宏” 一节)。

Other duplication includes the similar implementations of the request_review and approve methods on Post. Both methods use Option::take with the state field of Post, and if state is Some, they delegate to the wrapped value’s implementation of the same method and set the new value of the state field to the result. If we had a lot of methods on Post that followed this pattern, we might consider defining a macro to eliminate the repetition (see the “Macros” section in Chapter 20).

通过完全按照为面向对象语言定义的方式来实现状态模式,我们并没有像我们本可以做到的那样充分利用 Rust 的优势。让我们看看我们可以对 blog crate 做些什么改变,使无效的状态和转换变成编译时错误。

By implementing the state pattern exactly as it’s defined for object-oriented languages, we’re not taking as full advantage of Rust’s strengths as we could. Let’s look at some changes we can make to the blog crate that can make invalid states and transitions into compile-time errors.

将状态和行为编码为类型

Encoding States and Behavior as Types

我们将向你展示如何重新思考状态模式,以获得一套不同的权衡。与其完全封装状态和转换以便外部代码对其一无所知,我们将把状态编码到不同的类型中。因此,Rust 的类型检查系统将通过发布编译器错误来防止在只允许已发布文章的地方尝试使用草稿文章。

We’ll show you how to rethink the state pattern to get a different set of tradeoffs. Rather than encapsulating the states and transitions completely so that outside code has no knowledge of them, we’ll encode the states into different types. Consequently, Rust’s type-checking system will prevent attempts to use draft posts where only published posts are allowed by issuing a compiler error.

让我们考虑示例 18-11 中 main 的第一部分:

Let’s consider the first part of main in Listing 18-11:

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());
}

我们仍然支持使用 Post::new 创建草稿状态下的新文章,以及向文章内容添加文本的能力。但我们不再在草稿文章上提供返回空字符串的 content 方法,而是让草稿文章根本没有 content 方法。这样一来,如果我们尝试获取草稿文章的内容,我们就会得到一个告知我们该方法不存在的编译器错误。因此,我们就不可能在生产环境中意外显示草稿文章内容,因为那段代码甚至根本无法编译。示例 18-19 展示了 Post 结构体和 DraftPost 结构体的定义,以及各自的方法。

We still enable the creation of new posts in the draft state using Post::new and the ability to add text to the post’s content. But instead of having a content method on a draft post that returns an empty string, we’ll make it so that draft posts don’t have the content method at all. That way, if we try to get a draft post’s content, we’ll get a compiler error telling us the method doesn’t exist. As a result, it will be impossible for us to accidentally display draft post content in production because that code won’t even compile. Listing 18-19 shows the definition of a Post struct and a DraftPost struct, as well as methods on each.

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);
    }
}

PostDraftPost 结构体都有一个存储博客文章文本的私有 content 字段。这些结构体不再有 state 字段,因为我们将状态的编码移到了结构体的类型中。 Post 结构体将代表一篇已发布的文章,它有一个返回 contentcontent 方法。

Both the Post and DraftPost structs have a private content field that stores the blog post text. The structs no longer have the state field because we’re moving the encoding of the state to the types of the structs. The Post struct will represent a published post, and it has a content method that returns the content.

我们仍然有一个 Post::new 函数,但它不再返回 Post 的实例,而是返回 DraftPost 的实例。由于 content 是私有的,并且没有任何函数返回 Post,因此目前无法直接创建 Post 实例。

We still have a Post::new function, but instead of returning an instance of Post, it returns an instance of DraftPost. Because content is private and there aren’t any functions that return Post, it’s not possible to create an instance of `Post right now.

DraftPost 结构体有一个 add_text 方法,所以我们可以像以前一样向 content 添加文本,但请注意,DraftPost 并没有定义 content 方法!所以现在程序确保了所有文章都以草稿文章开始,而草稿文章的内容不可用于显示。任何试图绕过这些约束的尝试都会导致编译器错误。

The DraftPost struct has an add_text method, so we can add text to content as before, but note that DraftPost does not have a content method defined! So now the program ensures that all posts start as draft posts, and draft posts don’t have their content available for display. Any attempt to get around these constraints will result in a compiler error.

那么,我们如何得到一篇已发布的文章呢?我们要强制执行这样的规则:草稿文章必须经过审核和批准才能发布。处于等待审核状态的文章仍不应显示任何内容。让我们通过添加另一个结构体 PendingReviewPost 来实现这些约束,在 DraftPost 上定义 request_review 方法以返回 PendingReviewPost,并在 PendingReviewPost 上定义 approve 方法以返回 Post,如示例 18-20 所示。

So, how do we get a published post? We want to enforce the rule that a draft post has to be reviewed and approved before it can be published. A post in the pending review state should still not display any content. Let’s implement these constraints by adding another struct, PendingReviewPost, defining the request_review method on DraftPost to return a PendingReviewPost and defining an approve method on PendingReviewPost to return a Post, as shown in Listing 18-20.

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 {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

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

request_reviewapprove 方法获取 self 的所有权,从而消耗 DraftPostPendingReviewPost 实例,并将它们分别转换为 PendingReviewPost 和已发布的 Post。这样,在我们对 DraftPost 实例调用 request_review 之后,就不会剩下任何残余的实例,依此类推。 PendingReviewPost 结构体上没有定义 content 方法,因此尝试读取其内容会导致编译器错误,就像 DraftPost 一样。因为获取定义了 content 方法的已发布 Post 实例的唯一方法是在 PendingReviewPost 上调用 approve 方法,而获取 PendingReviewPost 的唯一方法是在 DraftPost 上调用 request_review 方法,我们现在已经将博客文章工作流编码到了类型系统中。

The request_review and approve methods take ownership of self, thus consuming the DraftPost and PendingReviewPost instances and transforming them into a PendingReviewPost and a published Post, respectively. This way, we won’t have any lingering DraftPost instances after we’ve called request_review on them, and so forth. The PendingReviewPost struct doesn’t have a content method defined on it, so attempting to read its content results in a compiler error, as with DraftPost. Because the only way to get a published Post instance that does have a content method defined is to call the approve method on a PendingReviewPost, and the only way to get a PendingReviewPost is to call the request_review method on a DraftPost, we’ve now encoded the blog post workflow into the type system.

但我们也必须对 main 进行一些细微的修改。 request_reviewapprove 方法返回新实例,而不是修改调用它们的结构体,因此我们需要添加更多的 let post = 遮蔽(shadowing)赋值来保存返回的实例。我们也不能再让关于草稿和等待审核的文章内容为空字符串的断言存在,也不需要它们:我们无法再编译尝试使用这些状态下文章内容的代码。更新后的 main 代码如示例 18-21 所示。

But we also have to make some small changes to main. The request_review and approve methods return new instances rather than modifying the struct they’re called on, so we need to add more let post = shadowing assignments to save the returned instances. We also can’t have the assertions about the draft and pending review posts’ contents be empty strings, nor do we need them: We can’t compile code that tries to use the content of posts in those states any longer. The updated code in main is shown in Listing 18-21.

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());
}

我们需要对 main 做出的重新赋值 post 的更改意味着,此实现不再完全遵循面向对象的状态模式:状态之间的转换不再完全封装在 Post 实现中。然而,我们的收获是由于类型系统和编译时发生的类型检查,现在不可能出现无效状态!这确保了某些 bug,例如显示未发布文章的内容,在进入生产环境之前就会被发现。

The changes we needed to make to main to reassign post mean that this implementation doesn’t quite follow the object-oriented state pattern anymore: The transformations between the states are no longer encapsulated entirely within the Post implementation. However, our gain is that invalid states are now impossible because of the type system and the type checking that happens at compile time! This ensures that certain bugs, such as display of the content of an unpublished post, will be discovered before they make it to production.

尝试本节开头针对 blog crate(示例 18-21 之后的样子)建议的任务,看看你对这个版本的代码设计有何看法。请注意,在这一设计中,某些任务可能已经完成了。

Try the tasks suggested at the start of this section on the blog crate as it is after Listing 18-21 to see what you think about the design of this version of the code. Note that some of the tasks might be completed already in this design.

我们已经看到,即使 Rust 能够实现面向对象设计模式,其他模式(如将状态编码到类型系统中)在 Rust 中也是可用的。这些模式具有不同的权衡。虽然你可能非常熟悉面向对象模式,但重新思考问题以利用 Rust 的特性可以带来好处,例如在编译时防止某些 bug。由于 Rust 拥有面向对象语言所没有的某些特性(如所有权),面向对象模式在 Rust 中并不总是最佳解决方案。

We’ve seen that even though Rust is capable of implementing object-oriented design patterns, other patterns, such as encoding state into the type system, are also available in Rust. These patterns have different trade-offs. Although you might be very familiar with object-oriented patterns, rethinking the problem to take advantage of Rust’s features can provide benefits, such as preventing some bugs at compile time. Object-oriented patterns won’t always be the best solution in Rust due to certain features, like ownership, that object-oriented languages don’t have.

总结

Summary

无论你在读完本章后是否认为 Rust 是一门面向对象的语言,你现在都知道了可以在 Rust 中使用 trait 对象来获得某些面向对象特性。动态分派可以给你的代码带来一些灵活性,代价是一点运行时性能。你可以利用这种灵活性来实现面向对象模式,从而帮助提高代码的可维护性。Rust 还拥有面向对象语言所没有的其他特性,比如所有权。面向对象模式并不总是利用 Rust 优势的最佳方式,但它是一个可用的选项。

Regardless of whether you think Rust is an object-oriented language after reading this chapter, you now know that you can use trait objects to get some object-oriented features in Rust. Dynamic dispatch can give your code some flexibility in exchange for a bit of runtime performance. You can use this flexibility to implement object-oriented patterns that can help your code’s maintainability. Rust also has other features, like ownership, that object-oriented languages don’t have. An object-oriented pattern won’t always be the best way to take advantage of Rust’s strengths, but it is an available option.

接下来,我们将研究模式(patterns),这是 Rust 的另一个能提供极大灵活性的特性。我们在全书中已经简要地了解过它们,但尚未看到它们的全部本领。走起!

Next, we’ll look at patterns, which are another of Rust’s features that enable lots of flexibility. We’ve looked at them briefly throughout the book but haven’t seen their full capability yet. Let’s go!