@ -1,36 +1,23 @@
## 面向对象设计模式的实现
让我们看一下状态设计模式和怎样在Rust中来使用它的例子. *状态模式* 就是当一个值有多个内部状态时,值的行为改变基于内部状态. Let's look at an example of the state design pattern and how to use it in Rust.
The *state pattern* is when a value has some internal state, and the value's
behavior changes based on the internal state. The internal state is represented
by a set of objects that inherit shared functionality (we'll use structs and
traits since Rust doesn't have objects and inheritance). Each state object is
responsible for its own behavior and the rules for when it should change into
another state. The value that holds one of these state objects doesn't know
anything about the different behavior of the states or when to transition
between states. In the future when requirements 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.
In order to explore this idea, we're going to implement a blog post workflow in
an incremental way. The workflow that we want our blog posts to follow, once
we're done with the implementation, is:
1. A blog post starts as an empty draft.
2. Once the draft is done, we request a review of the post.
3. Once the post is approved, it gets published.
4. Only published blog posts return content to print so that we can't
accidentally print the text of a post that hasn't been approved.
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 stay an unpublished draft.
Listing 17-11 shows this workflow in code form. This is an example usage of the
API we're going to implement in a library crate named `blog` :
< span class = "filename" > Filename: src/main.rs< / span >
> [ch17-03-oo-design-patterns.md ](https://github.com/rust-lang/book/blob/master/second-edition/src/ch17-03-oo-design-patterns.md )
> < br >
> commit 67737ff868e3347588cc832eceb8fc237afc5895
让我们看看一个状态设计模式的例子以及如何在 Rust 中使用他们。**状态模式**( *state pattern*)是指一个值有某些内部状态,而它的行为随着其内部状态而改变。内部状态由一系列继承了共享功能的对象表现(我们使用结构体和 trait 因为 Rust 没有对象和继承)。每一个状态对象负责它自身的行为和当需要改变为另一个状态时的规则。持有任何一个这种状态对象的值对于不同状态的行为以及何时状态转移毫不知情。当将来需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变它的规则,或者是增加更多的状态对象。
为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个我们希望发布博文时所应遵守的工作流,一旦完成了它的实现,将为如下:
1. 博文从空白的草案开始。
2. 一旦草案完成,请求审核博文。
3. 一旦博文过审,它将被发表。
4. 只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本。
任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。
列表 17-11 展示这个工作流的代码形式。这是一个我们将要在一个叫做 `blog` 的库 crate 中实现的 API 的使用示例:
< span class = "filename" > 文件名: src/main.rs< / span >
```rust,ignore
extern crate blog;
@ -50,44 +37,19 @@ fn main() {
}
```
< span class = "caption" > Listing 17-11: Code that demonstrates the desired
behavior we want our `blog` crate to have</ span >
We want to be able to create a new draft blog post with `Post::new` . Then, we
want to add some text to the blog post while we're in the draft state. If we
try to print out the post's content immediately, though, we shouldn't get any
text, since the post is still a draft. We've added an `assert_eq!` here for
demonstration purposes. Asserting that a draft blog post returns an empty
string from the `content` method would make an excellent unit test in our
library, but we're not going to write tests for this example.
Next, we want to be able to request a review of our post, and `content` should
still return an empty string while waiting for a review. Lastly, when we
approve the blog post, it should get published, which means the text we added
will be returned when we call `content` .
Notice that the only type we're interacting with from the crate is the `Post`
type. The various states a post can be in (draft, waiting for review,
published) are managed internally to the `Post` type. The states change due to
the methods we call on the `Post` instance, but we don't have to manage the
state changes directly. This also means we won't make a mistake with the
states, like forgetting to request a review before publishing.
### Defining `Post` and Creating a New Instance in the Draft State
Let's get started on the implementation of the library! We know we want to have
a public `Post` struct that holds some content, so let's start with the
definition of the struct and an associated public `new` function to create an
instance of `Post` as shown in Listing 17-12. We're also going to have a
private trait `State` . `Post` will hold a trait object of `Box<State>` inside
an `Option` in a private field named `state` . We'll see why the `Option` is
necessary in a bit. The `State` trait defines all the behavior different post
states share, and the `Draft` , `PendingReview` , and `Published` states will all
implement the `State` trait. For now, the trait does not have any methods, and
we're going to start by defining just the `Draft` state since that's the state
we want to start in:
< span class = "filename" > Filename: src/lib.rs< / span >
< span class = "caption" > 列表 17-11: 展示了 `blog` crate 期望行为的代码</ span >
我们希望能够使用 `Post::new` 创建一个新的博文草案。接着希望能在草案阶段为博文编写一些文本。如果尝试立即打印出博文的内容,将不会得到任何文本,因为博文仍然是草案。这里增加的 `assert_eq!` 用于展示目的。断言草案博文的 `content` 方法返回空字符串将能作为库的一个非常好的单元测试,不过我们并不准备为这个例子编写单元测试。
接下来,我们希望能够请求审核博文,而在等待审核的阶段 `content` 应该仍然返回空字符串,当博文审核通过,它应该被发表,这意味着当调用 `content` 时我们编写的文本将被返回。
注意我们与 crate 交互的唯一的类型是 `Post` 。博文可能处于的多种状态(草案,等待审核和发布)由 `Post` 内部管理。博文状态依我们在`Post`调用的方法而改变,但不必直接管理状态改变。这也意味着不会在状态上犯错,比如忘记了在发布前请求审核。
### 定义 `Post` 并新建一个草案状态的实例
让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如列表 17-12 所示。我们还需定义一个私有 trait `State` 。`Post` 将在私有字段 `state` 中存放一个 `Option` 中的 trait 对象 `Box<State>` 。稍后将会看到为何 `Option` 是必须的。`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft` 、`PendingReview` 和 `Published` 状态都会实现`State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义`Draft`状态因为这是我们希望开始的状态:
< span class = "filename" > 文件名: src/lib.rs< / span >
```rust
pub struct Post {
@ -111,28 +73,16 @@ struct Draft {}
impl State for Draft {}
```
< span class = "caption" > Listing 17-12: Definition of a `Post` struct and a `new`
function that creates a new `Post` instance, a `State` trait, and a `Draft`
struct that implements `State` </ span >
< span class = "caption" > 列表 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和实现了 `State` 的结构体 `Draft` </ span >
When we create a new `Post` , we set its `state` field to a `Some` value holding
a `Box` pointing to a new instance of the `Draft` struct. This ensures whenever
we create a new instance of `Post` , it'll start out as a draft. Because the
`state` field of `Post` is private, there's no way to create a `Post` in any
other state!
当创建新的 `Post` 时,我们将其 `state` 字段设置为一个 `Some` 值,它存放了指向一个 `Draft` 结构体新实例的 `Box` 。这确保了无论何时新建一个 `Post` 实例,它会从草案开始。因为 `Post` 的 `state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。
### Storing the Text of the Post Content
### 存放博文内容的文本
In the `Post::new` function, we set the `content` field to a new, empty
`String` . In Listing 17-11, we showed that we want to be able to call a method
named `add_text` and pass a `&str` to it to add that text to the content of the
blog post. We're choosing to implement this as a method rather than exposing
the `content` field as `pub` because we want to be able to control how the
`content` field's data is read by implementing a method later. The `add_text`
method is pretty straightforward though, let's add the implementation in
Listing 17-13 to the `impl Post` block:
在 `Post::new` 函数中,我们设置 `content` 字段为新的空 `String` 。在列表 17-11 中,展示了我们希望能够调用一个叫做 `add_text` 的方法并向其传递一个 `&str` 来将文本增加到博文的内容中。选择实现为一个方法而不是将 `content` 字段暴露为 `pub` 是因为我们希望能够通过之后实现的一个方法来控制 `content` 字段如何被读取。`add_text` 方法是非常直观的,让我们在列表 17-13 的 `impl Post` 块中增加一个实现:
< span class = "filename" > Filename: src/lib.rs< / span >
< span class = "filename" > 文件名: src/lib.rs< / span >
```rust
# pub struct Post {
@ -147,30 +97,15 @@ impl Post {
}
```
< span class = "caption" > Listing 17-13: Implementing the `add_text` method to add
text to a post's `content` </ span >
< span class = "caption" > 列表 17-13: 实现方法 `add_text` 来向博文的 `content` 增加文本</ span >
`add_text` takes a mutable reference to `self` , since 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 isn't part of the state pattern since its behavior doesn't
depend on the state that the post is in. The `add_text` method doesn't interact
with the `state` field at all, but it is part of the behavior we want to
support.
`add_text` 获取一个 `self` 的可变引用,因为需要改变调用 `add_text` 的 `Post` 。接着调用 `content` 中的 `String` 的 `push_str` 并传递 `text` 参数来保存到 `content` 中。这不是状态模式的一部分,因为它的行为并不依赖博文所处的状态。`add_text` 方法完全不与 `state` 状态交互,不过这是我们希望支持的行为的一部分。
### Content of a Draft Post is Empty
### 博文草案的内容是空的
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 since the post is still in
the draft state, as shown on line 8 of Listing 17-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're going to change this later once
we implement the ability to change a post's state to be published. With what we
have so far, though, posts can only be in the draft state, which means the post
content should always be empty. Listing 17-14 shows this placeholder
implementation:
调用 `add_text` 并像博文增加一些内容之后,我们仍然希望 `content` 方法返回一个空字符串 slice, 因为博文仍然处于草案状态, 如列表 17-11 的第 8 行所示。现在让我们使用能满足要求的最简单的方式来实现 `content` 方法 总是返回一个空字符 slice。当实现了将博文状态改为发布的能力之后将改变这一做法。但是现在博文只能是草案状态, 这意味着其内容总是空的。列表 17-14 展示了这个占位符实现:
< span class = "filename" > Filename : src/lib.rs< / span >
< span class = "filename" > 文件名: src/lib.rs< / span >
```rust
# pub struct Post {
@ -185,27 +120,15 @@ impl Post {
}
```
< span class = "caption" > Listing 17-14: Adding a placeholder implementation for
the `content` method on `Post` that always returns an empty string slice</ span >
< span class = "caption" > 列表 17-14: 增加一个 `Post` 的 `content` 方法的占位实现,它总是返回一个空字符串 slice</ span >
With this added `content` method, everything in Listing 17-11 up to line 8
works as we intend.
通过增加这个 `content` 方法,列表 17-11 中直到第 8 行的代码能如期运行。
### Requesting a Review of the Post Changes its State
### 请求审核博文来改变其状态
Next up is requesting a review of a post, which should change its state from
`Draft` to `PendingReview` . We want `post` to have a public method named
`request_review` that will take a mutable reference to `self` . Then we're going
to call an internal `request_review` method on the state that we're holding, and
this second `request_review` method will consume the current state and return a
new state. In order to be able to consume the old state, the second `request_review`
method needs to take ownership of the state value. This is where the `Option` comes
in: we're going to `take` the `Some` value out of the `state` field and leave a
`None` in its place since Rust doesn't let us have unpopulated fields in
structs. Then we'll set the post's `state` value to the result of this
operation. Listing 17-15 shows this code:
接下来是请求审核博文,这应当将其状态由 `Draft` 改为 `PendingReview` 。我们希望 `post` 有一个获取 `self` 可变引用的公有方法 `request_review` 。接着将调用内部存放的状态的 `request_review` 方法,而这第二个 `request_review` 方法会消费当前的状态并返回要一个状态。为了能够消费旧状态,第二个 `request_review` 方法需要能够获取状态值的所有权。这就是 `Option` 的作用:我们将 `take` 字段 `state` 中的 `Some` 值并留下一个 `None` 值,因为 Rust 并不允许结构体中有空字段。接着将博文的 `state` 设置为这个操作的结果。列表 17-15 展示了这些代码:
< span class = "filename" > Filename : src/lib.rs< / span >
< span class = "filename" > 文件名: src/lib.rs< / span >
```rust
# pub struct Post {
@ -243,451 +166,6 @@ impl State for PendingReview {
}
```
< span class = "caption" > Listing 17-15: Implementing `request_review` methods on
`Post` and the `State` trait</ span >
We've added 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>` , which is what we want because we're transforming the
old state into a new state, and we want the old state to no longer be valid.
The implementation for the `request_review` method on `Draft` is to return a
new, boxed instance of the `PendingReview` struct, which is a new type we've
introduced that represents the state when a post is waiting for a review. The
`PendingReview` struct also implements the `request_review` method, but it
doesn't do any transformations. It returns itself since requesting a review on
a post that's already in the `PendingReview` state should stay in the
`PendingReview` state.
Now we can start seeing the advantages of the state pattern: the
`request_review` method on `Post` is the same no matter what its `state` value
is. Each state is responsible for its own rules.
We're going to leave the `content` method on `Post` as it is, returning an
empty string slice. We can now have a `Post` in the `PendingReview` state, not
just the `Draft` state, but we want the same behavior in the `PendingReview`
state. Listing 17-11 now works up until line 11!
### Approving a Post Changes the Behavior of `content`
The `approve` method on `Post` will be similar to that of the `request_review`
method: it will set the `state` to the value that the current state says it
should have when that state is approved. We'll need to add the `approve` method
to the `State` trait, and we'll add a new struct that implements `State` , the
`Published` state. Listing 17-16 shows the new code:
< span class = "filename" > Filename: src/lib.rs< / span >
```rust
# pub struct Post {
# state: Option< Box < State > >,
# content: String,
# }
#
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< State > ;
fn approve(self: Box< Self > ) -> Box< State > ;
}
struct Draft {}
impl State for Draft {
# fn request_review(self: Box< Self > ) -> Box< State > {
# Box::new(PendingReview {})
# }
#
// ...snip...
fn approve(self: Box< Self > ) -> Box< State > {
self
}
}
struct PendingReview {}
impl State for PendingReview {
# fn request_review(self: Box< Self > ) -> Box< State > {
# Box::new(PendingReview {})
# }
#
// ...snip...
fn approve(self: Box< Self > ) -> Box< State > {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box< Self > ) -> Box< State > {
self
}
fn approve(self: Box< Self > ) -> Box< State > {
self
}
}
```
< span class = "caption" > Listing 17-16: Implementing the `approve` method on
`Post` and the `State` trait</ span >
Similarly to `request_review` , if we call the `approve` method on a `Draft` , it
will have no effect since it 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 since the
post should stay in the `Published` state in those cases.
Now for updating the `content` method on `Post` : we want to return the value in
the post's `content` field if its state is `Published` , otherwise we want to
return an empty string slice. Because the goal is to keep all the rules like
this in the structs that implement `State` , we're going to call a `content`
method on the value in `state` and pass the post instance (that is, `self` ) as
an argument. Then we'll return the value returned from the `content` method on
the `state` value as shown in Listing 17-17:
< span class = "filename" > Filename: src/lib.rs< / span >
```rust
# trait State {
# fn content< 'a>(& self, post: & 'a Post) -> & 'a str;
# }
# pub struct Post {
# state: Option< Box < State > >,
# content: String,
# }
#
impl Post {
// ...snip...
pub fn content(& self) -> & str {
self.state.as_ref().unwrap().content(& self)
}
// ...snip...
}
```
< span class = "caption" > Listing 17-17: Updating the `content` method on `Post` to
delegate to a `content` method on `State` </ span >
We're calling the `as_ref` method on the `Option` because we want a reference
to the value inside the `Option` . We're then calling the `unwrap` method, which
we know will never panic because all the methods on `Post` ensure that the
`state` value will have a `Some` value in it when those methods are done. This
is one of the cases we talked about in Chapter 12 where we know that a `None`
value is never possible even though the compiler isn't able to understand that.
The `content` method on the `State` trait is where the logic for what content
to return will be. We're going to add a default implementation for the
`content` method that returns an empty string slice. That lets us not need to
implement `content` on the `Draft` and `PendingReview` structs. The `Published`
struct will override the `content` method and will return the value in
`post.content` , as shown in Listing 17-18:
< span class = "filename" > Filename: src/lib.rs< / span >
```rust
# pub struct Post {
# content: String
# }
trait State {
// ...snip...
fn content< 'a>(& self, post: & 'a Post) -> & 'a str {
""
}
}
// ...snip...
struct Published {}
impl State for Published {
// ...snip...
fn content< 'a>(& self, post: & 'a Post) -> & 'a str {
& post.content
}
}
```
< span class = "caption" > Listing 17-18: Adding the `content` method to the `State`
trait< / span >
Note that we need lifetime annotations on this method, like we discussed in
Chapter 10. We're taking a reference to a `post` as an argument, and we're
returning a reference to a part of that `post` , so the lifetime of the returned
reference is related to the lifetime of the `post` argument.
### Tradeoffs of the State Pattern
We've shown that Rust is capable of implementing the object-oriented state
pattern in order to encapsulate the different kinds of behavior that a post
should have that depends on the state that the post is in. The methods on
`Post` don't know anything about the different kinds of behavior. The way this
code is organized, we have one place to look in order to find out all the
different ways that a published post behaves: the implementation of the `State`
trait on the `Published` struct.
An alternative implementation that didn't use the state pattern might have
`match` statements in the methods on `Post` or even in the code that uses
`Post` (`main` in our case) that checks what the state of the post is and
changes behavior in those places instead. That would mean we'd have a lot of
places to look in order to understand all the implications of a post being in
the published state! This would get worse the more states we added: each of
those `match` statements would need another arm. With the state pattern, the
`Post` methods and the places we use `Post` don't need `match` statements and
adding a new state only involves adding a new `struct` and implementing the
trait methods on that one struct.
This implementation is easy to extend to add more functionality. Here are some
changes you can try making to the code in this section to see for yourself what
it's like to maintain code using this pattern over time:
- Only allow adding text content when a post is in the `Draft` state
- Add a `reject` method that changes the post's state from `PendingReview` back
to `Draft`
- Require two calls to `approve` before changing the state to `Published`
A downside of the state pattern is that since the states implement the
transitions between the 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 nicer if `PendingReview` wouldn't need to
change because of the addition of a new state, but that would mean switching to
another design pattern.
There are a few bits of duplicated logic that are a downside of this
implementation in Rust. It would be nice if we could make default
implementations for the `request_review` and `approve` methods on the `State`
trait that return `self` , but this would violate object safety since the trait
doesn't know what the concrete `self` will be exactly. We want to be able to
use `State` as a trait object, so we need its methods to be object safe.
The other duplication that would be nice to get rid of is the similar
implementations of the `request_review` and `approve` methods on `Post` . They
both delegate to the implementation of the same method on the value in the
`Option` in the `state` field, 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 Appendix E on
macros).
A downside of implementing this object-oriented pattern exactly as it's defined
for object-oriented languages is that we're not taking advantage of Rust's
strengths as much as we could be. Let's take a look at some changes we can make
to this code that can make invalid states and transitions into compile time
errors.
#### Encoding States and Behavior as Types
We're going to show how to rethink the state pattern a bit in order 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're
going to encode the states into different types. When the states are types,
Rust's type checking will make any attempt to use a draft post where we should
only use published posts into a compiler error.
Let's consider the first part of `main` from Listing 17-11:
< span class = "filename" > Filename: src/main.rs< / span >
```rust,ignore
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
}
```
We still want to create a new post in the draft state using `Post::new` , and we
still want to be able 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're going to
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 that the
method doesn't exist. This will make it impossible for us to accidentally
display draft post content in production, since that code won't even compile.
Listing 17-19 shows the definition of a `Post` struct, a `DraftPost` struct,
and methods on each:
< span class = "filename" > Filename: src/lib.rs< / span >
```rust
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);
}
}
```
< span class = "caption" > Listing 17-19: A `Post` with a `content` method and a
`DraftPost` without a `content` method</ span >
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 since we're moving
the encoding of the state to the types of the structs. `Post` will represent a
published post, and it has a `content` method that returns the `content` .
We still have a `Post::new` function, but instead of returning an instance of
`Post` , it returns an instance of `DraftPost` . It's not possible to create an
instance of `Post` right now since `content` is private and there aren't any
functions that return `Post` . `DraftPost` has an `add_text` method defined on
it so that we can add text to `content` as before, but note that `DraftPost`
does not have a `content` method defined! So we've enforced 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 be a compiler error.
#### Implementing Transitions as Transformations into Different Types
So how do we get a published post then? The rule we want to enforce is 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 17-20:
< span class = "filename" > Filename: src/lib.rs< / span >
```rust
# pub struct Post {
# content: String,
# }
#
# pub struct DraftPost {
# content: String,
# }
#
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,
}
}
}
```
< span class = "caption" > Listing 17-20: A `PendingReviewPost` that gets created by
calling `request_review` on `DraftPost` , and an `approve` method that turns a
`PendingReviewPost` into a published `Post` </ span >
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 `DraftPost` instances lingering around after we've called
`request_review` on them, and so forth. `PendingReviewPost` doesn't have a
`content` method defined on it, so attempting to read its content is a compiler
error like it is 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.
This does mean we have to make some small changes to `main` . Because
`request_review` and `approve` return new instances rather than modifying the
struct they're called on, 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 post's contents being empty string anymore,
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 17-21:
< span class = "filename" > Filename: src/main.rs< / span >
```rust,ignore
extern crate blog;
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());
}
```
< span class = "caption" > 列表 17-15: 实现 `Post` 和 `State` trait 的 `request_review` 方法</ span >
< span class = "caption" > Listing 17-21: Modifications to `main` to use the new
implementation of the blog post workflow< / span >
Having to change `main` to reassign `post` is what makes this implementation
not quite following the object-oriented state pattern anymore: the
transformations between the states are no longer encapsulated entirely within
the `Post` implementation. However, we've gained the property of having invalid
states be impossible because of the type system and type checking that happens
at compile time! This ensures that certain bugs, such as displaying the content
of an unpublished post, will be discovered before they make it to production.
Try the tasks suggested that add additional requirements that we mentioned at
the start of this section to see how working with this version of the code
feels.
Even though Rust is capable of implementing object-oriented design patterns,
there are other patterns like encoding state into the type system that are
available in Rust. These patterns have different tradeoffs than the
object-oriented patterns do. While you may be very familiar with
object-oriented patterns, rethinking the problem in order to take advantage of
Rust's features can give benefits like preventing some bugs at compile-time.
Object-oriented patterns won't always be the best solution in Rust, since Rust
has features like ownership that object-oriented languages don't have.
## Summary
No matter whether you think Rust is an object-oriented language or not after
reading this chapter, you've now seen that trait objects are a way to get some
object-oriented features in Rust. Dynamic dispatch can give your code some
flexibility in exchange for a bit of runtime performance. This flexibility can
be used to implement object-oriented patterns that can help with the
maintainability of your code. Rust also has different features, like ownership,
than object-oriented languages. An object-oriented pattern won't always be the
best way to take advantage of Rust's strengths.
Next, let's look at another feature of Rust that enables lots of flexibility:
patterns. We've looked at them briefly throughout the book, but haven't seen
everything they're capable of yet. Let's go!
这里给 `State` trait 增加了 `request_review` 方法;所有实现了这个 trait 的类型现在都需要实现 `request_review` 方法。注意不用于使用`self`、 `&self` 或者 `&mut self` 作为方法的第一个参数,这里使用了 `self: Box<Self>` 。这个语法意味着