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


x-i18n: generated_at: “2026-03-01T14:53:38Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 2f87e2bda9a748c973642b0c7f9ded2ac0467059a708aecabe7bd11e08eb305a source_path: ch18-03-oo-design-patterns.md workflow: 16

实现面向对象的设计模式 (Implementing an Object-Oriented Design Pattern)

Implementing an Object-Oriented Design Pattern

“状态模式 (state pattern)”是一种面向对象的设计模式。该模式的要点是,我们定义一个值内部可以拥有的一组状态。状态由一组“状态对象 (state objects)”表示,并且值的行为根据其状态而改变。我们将通过一个博客文章结构体的例子来演示,该结构体有一个字段来持有其状态,状态将是“草稿 (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 中,我们使用结构体和特征而不是对象和继承。每个状态对象负责其自身的行为,并管理何时应转变为另一种状态。持有状态对象的值对状态的不同行为或何时在状态之间切换一无所知。

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.

最终的功能将如下所示:

  1. 一篇博客文章从空白草稿开始。
  2. 草稿完成后,请求对文章进行审核。
  3. 文章获批后,即被发布。
  4. 只有已发布的博客文章才会返回要打印的内容,这样未获批的文章就不会被意外发布。

The final functionality will look like this:

  1. A blog post starts as an empty draft.
  2. When the draft is done, a review of the post is requested.
  3. When the post is approved, it gets published.
  4. 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)

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 11-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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:all}}

我们希望允许用户通过 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 特征,它将定义所有 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> 特征对象,以此持有状态对象。稍后你就会明白为什么 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-12/src/lib.rs}}

State 特征定义了不同文章状态共享的行为。状态对象是 DraftPendingReviewPublished ,它们都将实现 State 特征。目前,该特征没有任何方法,我们将从仅定义 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 方法非常简单,所以让我们在 impl Post 块中添加示例 18-13 的实现。

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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-13/src/lib.rs:here}}

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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-14/src/lib.rs:here}}

有了这个新增的 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-15/src/lib.rs:here}}

我们给 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 特征中;现在所有实现该特征的类型都需要实现 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 不允许我们在结构体中留有未填充的字段。这让我们可以将状态值移出 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(); 的代码来设置它,以便获得状态值的所有权。这确保了 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.

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

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 rules.

我们将保持 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-16/src/lib.rs:here}}

我们将 approve 方法添加到 State 特征中,并添加一个新的实现 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 特征,对于 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-17/src/lib.rs:here}}

因为目标是将所有这些规则保持在实现 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 ,我们将得到一个错误,因为我们不能将 state 移出函数参数中借用的 &self

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 方法,我们知道它永远不会引发恐慌,因为我们知道 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 时,解引用强制转换将在 &Box 上生效,以便最终对实现 State 特征的类型调用 content 方法。这意味着我们需要将 content 添加到 State 特征定义中,并在此放置根据我们所处的状态返回什么内容的逻辑,如示例 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-18/src/lib.rs:here}}

我们为 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 表达式或类似结构来处理每个可能的变体。这可能比这种特征对象解决方案更具重复性。

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)

Evaluating the State Pattern

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

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 表达式,并且要添加一个新状态,我们只需要在一个位置添加一个新的结构体并在该结构体上实现特征方法。

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.

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

  • 添加一个 reject 方法,将文章的状态从 PendingReview 变回 Draft
  • 要求两次调用 approve 才能将状态更改为 Published
  • 仅允许用户在文章处于 Draft 状态时添加文本内容。提示:让状态对象负责内容可能发生的变化,但不负责修改 Post

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:

  • 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 特征上为返回 selfrequest_reviewapprove 方法制作默认实现。然而,这行不通:当使用 State 作为特征对象时,特征并不知道具体的 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 trade-offs. 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 的第一部分:

{{#rustdoc_include ../listings/ch18-oop/listing-18-11/src/main.rs:here}}

我们仍然支持使用 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-19/src/lib.rs}}

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 上定义返回 PendingReviewPostrequest_review 方法,以及在 PendingReviewPost 上定义返回 Postapprove 方法来实现这些约束,如示例 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-20/src/lib.rs:here}}

request_reviewapprove 方法获取 self 的所有权,从而消耗 DraftPostPendingReviewPost 实例,并将它们分别转换为 PendingReviewPost 和已发布的 Post 。这样,在我们调用了 request_review 之后,就不会再有残留的 DraftPost 实例,以此类推。 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 = 遮蔽赋值来保存返回的实例。我们也不再需要(也无法拥有)关于草稿和待审核文章内容为空字符串的断言:我们再也无法编译尝试使用这些状态下文章内容的代码了。更新后的 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.

{{#rustdoc_include ../listings/ch18-oop/listing-18-21/src/main.rs}}

我们需要对 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 中并不总是最佳解决方案。

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 中获得一些面向对象的特性。动态分派可以给你的代码提供一些灵活性,以换取一点运行时性能。你可以利用这种灵活性来实现面向对象模式,从而有助于代码的可维护性。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!