实现面向对象的设计模式
*状态模式* 是一种面向对象的设计模式。该模式的核心在于我们定义了一个值在内部可以拥有的一组状态。这些状态由一组 *状态对象* 表示,并且值的行为会根据其状态而改变。我们将通过一个博客文章结构体的示例进行讲解,该结构体有一个字段来保存其状态,该状态将是 “草稿”、“审核中” 或 “已发布” 集合中的一个状态对象。
状态对象共享功能:当然,在 Rust 中,我们使用结构体和 trait 而不是对象和继承。每个状态对象负责其自身的行为,并负责管理何时应更改为另一种状态。持有状态对象的值对状态的不同行为或何时在状态之间转换一无所知。
使用状态模式的优点是,当程序的业务需求发生变化时,我们无需更改持有状态的值的代码或使用该值的代码。我们只需要更新其中一个状态对象内部的代码来更改其规则,或者可能添加更多状态对象。
首先,我们将以更传统的面向对象方式实现状态模式,然后我们将使用一种在 Rust 中更自然的方法。让我们深入研究如何使用状态模式逐步实现博客文章工作流程。
最终功能如下所示
- 博客文章以空草稿开始。
- 当草稿完成后,请求对文章进行审核。
- 当文章获得批准后,它将被发布。
- 只有已发布的博客文章才会返回要打印的内容,因此未经批准的文章不会被意外发布。
对文章尝试的任何其他更改都应无效。例如,如果我们尝试在请求审核之前批准一篇草稿博客文章,则该文章应保持未发布的草稿状态。
列表 17-11 以代码形式展示了这个工作流程:这是我们将在名为 `blog` 的库 crate 中实现的 API 的示例用法。由于我们尚未实现 `blog` crate,因此这还无法编译。
文件名:src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
列表 17-11:演示我们希望 `blog` crate 拥有的所需行为的代码
我们希望允许用户使用 `Post::new` 创建新的草稿博客文章。我们希望允许向博客文章添加文本。如果我们尝试在批准之前立即获取文章的内容,我们不应获得任何文本,因为该文章仍然是草稿。我们在代码中添加了 `assert_eq!` 以进行演示。对此的一个很好的单元测试是断言草稿博客文章从 `content` 方法返回一个空字符串,但我们不会为这个例子编写测试。
接下来,我们希望启用文章的审核请求,并且我们希望在等待审核时 `content` 返回一个空字符串。当文章获得批准后,它应该被发布,这意味着当调用 `content` 时,将返回文章的文本。
请注意,我们从 crate 中交互的唯一类型是 `Post` 类型。此类型将使用状态模式,并将持有一个值,该值将是代表文章可以处于的各种状态(草稿、等待审核或已发布)的三种状态对象之一。从一种状态到另一种状态的更改将在 `Post` 类型内部进行管理。状态会响应库用户在 `Post` 实例上调用的方法而更改,但他们不必直接管理状态更改。此外,用户不会在状态上犯错,例如在文章被审核之前就发布它。
定义 `Post` 并在草稿状态下创建新实例
让我们开始实现库!我们知道我们需要一个公共的 `Post` 结构体来保存一些内容,所以我们将从结构体的定义和一个关联的公共 `new` 函数开始,以创建一个 `Post` 实例,如列表 17-12 所示。我们还将创建一个私有的 `State` trait,它将定义 `Post` 的所有状态对象必须具有的行为。
然后,`Post` 将在名为 `state` 的私有字段中,在 `Option<T>` 内部持有一个 `Box<dyn State>` 的 trait 对象,以保存状态对象。稍后您将看到为什么 `Option<T>` 是必要的。
文件名:src/lib.rs
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 {}
列表 17-12:`Post` 结构体和创建一个新的 `Post` 实例的 `new` 函数、`State` trait 和 `Draft` 结构体的定义
`State` trait 定义了不同文章状态共享的行为。状态对象是 `Draft`、`PendingReview` 和 `Published`,它们都将实现 `State` trait。目前,该 trait 没有任何方法,我们将从仅定义 `Draft` 状态开始,因为这是我们希望文章开始所处的状态。
当我们创建一个新的 `Post` 时,我们将它的 `state` 字段设置为一个 `Some` 值,该值持有一个 `Box`。这个 `Box` 指向 `Draft` 结构体的新实例。这确保了每当我们创建一个新的 `Post` 实例时,它都将以草稿状态开始。由于 `Post` 的 `state` 字段是私有的,因此无法在任何其他状态下创建 `Post`!在 `Post::new` 函数中,我们将 `content` 字段设置为一个新的空 `String`。
存储文章内容的文本
我们在列表 17-11 中看到,我们希望能够调用一个名为 `add_text` 的方法,并向其传递一个 `&str`,然后将其作为博客文章的文本内容添加。我们将其实现为一个方法,而不是将 `content` 字段公开为 `pub`,这样我们稍后可以实现一个方法来控制如何读取 `content` 字段的数据。`add_text` 方法非常简单,因此让我们将列表 17-13 中的实现添加到 `impl Post` 代码块中
文件名:src/lib.rs
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 {}
列表 17-13:实现 `add_text` 方法以向文章的 `content` 添加文本
`add_text` 方法接受对 `self` 的可变引用,因为我们正在更改我们调用 `add_text` 的 `Post` 实例。然后,我们在 `content` 中的 `String` 上调用 `push_str`,并将 `text` 参数传递给它以添加到已保存的 `content` 中。此行为不依赖于文章所处的状态,因此它不是状态模式的一部分。`add_text` 方法根本不与 `state` 字段交互,但它是我们想要支持的行为的一部分。
确保草稿文章的内容为空
即使在我们调用 `add_text` 并向文章添加了一些内容之后,我们仍然希望 `content` 方法返回一个空字符串切片,因为该文章仍然处于草稿状态,如列表 17-11 的第 7 行所示。现在,让我们用最简单的方式实现 `content` 方法,以满足此要求:始终返回一个空字符串切片。一旦我们实现了更改文章状态使其可以发布的功能,我们将稍后更改此方法。到目前为止,文章只能处于草稿状态,因此文章内容应始终为空。列表 17-14 显示了这个占位符实现
文件名:src/lib.rs
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 {}
列表 17-14:为 `Post` 上的 `content` 方法添加占位符实现,该方法始终返回一个空字符串切片
有了这个添加的 `content` 方法,列表 17-11 中直到第 7 行的所有内容都按预期工作。
请求审核文章会更改其状态
接下来,我们需要添加请求审核文章的功能,这应该将其状态从 `Draft` 更改为 `PendingReview`。列表 17-15 显示了此代码
文件名:src/lib.rs
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
}
}
列表 17-15:在 `Post` 和 `State` trait 上实现 `request_review` 方法
我们为 `Post` 提供了一个名为 `request_review` 的公共方法,该方法将接受对 `self` 的可变引用。然后,我们在 `Post` 的当前状态上调用一个内部 `request_review` 方法,而第二个 `request_review` 方法会消耗当前状态并返回一个新状态。
我们将 `request_review` 方法添加到 `State` trait;所有实现该 trait 的类型现在都需要实现 `request_review` 方法。请注意,该方法的第一个参数不是 `self`、`&self` 或 `&mut self`,而是 `self: Box<Self>`。此语法意味着该方法仅在对持有该类型的 `Box` 调用时才有效。此语法获取 `Box<Self>` 的所有权,使旧状态失效,以便 `Post` 的状态值可以转换为新状态。
为了消耗旧状态,`request_review` 方法需要获取状态值的所有权。这就是 `Post` 的 `state` 字段中的 `Option` 的作用所在:我们调用 `take` 方法从 `state` 字段中取出 `Some` 值,并在其位置留下 `None`,因为 Rust 不允许我们在结构体中拥有未填充的字段。这使我们可以将 `state` 值移出 `Post` 而不是借用它。然后,我们将文章的 `state` 值设置为此操作的结果。
我们需要临时将 `state` 设置为 `None`,而不是使用类似 `self.state = self.state.request_review();` 的代码直接设置它,以获取 `state` 值的所有权。这确保了 `Post` 在我们将其转换为新状态后无法使用旧的 `state` 值。
`Draft` 上的 `request_review` 方法返回一个新的 `PendingReview` 结构体的新的 boxed 实例,该结构体表示文章正在等待审核的状态。`PendingReview` 结构体也实现了 `request_review` 方法,但没有进行任何转换。相反,它返回自身,因为当我们对已处于 `PendingReview` 状态的文章请求审核时,它应保持在 `PendingReview` 状态。
现在我们可以开始看到状态模式的优点:`Post` 上的 `request_review` 方法是相同的,无论其 `state` 值如何。每个状态都负责其自身的规则。
我们将 `Post` 上的 `content` 方法保持原样,返回一个空字符串切片。我们现在可以有一个处于 `PendingReview` 状态的 `Post` 以及处于 `Draft` 状态的 `Post`,但我们希望在 `PendingReview` 状态下具有相同的行为。列表 17-11 现在可以工作到第 10 行!
添加 `approve` 以更改 `content` 的行为
`approve` 方法将类似于 `request_review` 方法:它将 `state` 设置为当前状态在批准该状态时应具有的值,如列表 17-16 所示
文件名:src/lib.rs
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
}
}
列表 17-16:在 `Post` 和 `State` trait 上实现 `approve` 方法
我们将 `approve` 方法添加到 `State` trait,并添加一个新的结构体来实现 `State`,即 `Published` 状态。
类似于 `PendingReview` 上的 `request_review` 的工作方式,如果我们在 `Draft` 上调用 `approve` 方法,它将无效,因为 `approve` 将返回 `self`。当我们在 `PendingReview` 上调用 `approve` 时,它会返回一个新的 `Published` 结构体的新的 boxed 实例。`Published` 结构体实现了 `State` trait,并且对于 `request_review` 方法和 `approve` 方法,它都返回自身,因为在这种情况下,文章应保持在 `Published` 状态。
现在我们需要更新 `Post` 上的 `content` 方法。我们希望从 `content` 返回的值取决于 `Post` 的当前状态,因此我们将让 `Post` 委托给其 `state` 上定义的 `content` 方法,如列表 17-17 所示
文件名:src/lib.rs
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
}
}
列表 17-17:更新 `Post` 上的 `content` 方法以委托给 `State` 上的 `content` 方法
因为目标是将所有这些规则保留在实现 `State` 的结构体内部,所以我们在 `state` 中的值上调用 `content` 方法,并将文章实例(即 `self`)作为参数传递。然后,我们返回从在 `state` 值上使用 `content` 方法返回的值。
我们在 `Option` 上调用 `as_ref` 方法,因为我们想要对 `Option` 内部的值的引用,而不是值的所有权。因为 `state` 是一个 `Option<Box<dyn State>>`,所以当我们调用 `as_ref` 时,会返回一个 `Option<&Box<dyn State>>`。如果我们不调用 `as_ref`,我们将收到一个错误,因为我们无法将 `state` 移出函数参数的借用 `&self`。
然后我们调用 `unwrap` 方法,我们知道它永远不会 panic,因为我们知道 `Post` 上的方法确保在这些方法完成时 `state` 将始终包含一个 `Some` 值。这是我们在 “您比编译器拥有更多信息的情况”第 9 章的章节中讨论的情况之一,当我们知道 `None` 值永远不可能出现时,即使编译器无法理解这一点。
此时,当我们在 `&Box<dyn State>` 上调用 `content` 时,解引用强制转换将对 `&` 和 `Box` 生效,因此 `content` 方法最终将在实现 `State` trait 的类型上调用。这意味着我们需要将 `content` 添加到 `State` trait 定义中,这将是我们根据我们拥有的状态放置要返回的内容的逻辑的地方,如列表 17-18 所示
文件名:src/lib.rs
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
}
}
列表 17-18:将 `content` 方法添加到 `State` trait
我们为 `content` 方法添加一个默认实现,该方法返回一个空字符串切片。这意味着我们不需要在 `Draft` 和 `PendingReview` 结构体上实现 `content`。`Published` 结构体将覆盖 `content` 方法并返回 `post.content` 中的值。
请注意,我们需要在此方法上添加生命周期注释,正如我们在第 10 章中讨论的那样。我们正在将对 `post` 的引用作为参数,并返回对该 `post` 的一部分的引用,因此返回的引用的生命周期与 `post` 参数的生命周期相关。
我们完成了 - 列表 17-11 的所有内容现在都可以工作了!我们已经使用博客文章工作流程的规则实现了状态模式。与规则相关的逻辑存在于状态对象中,而不是分散在整个 `Post` 中。
为什么不用枚举?
您可能一直在想为什么我们不使用 `enum`,并将不同的可能文章状态作为变体。这当然是一种可能的解决方案,请尝试一下并比较最终结果,看看您更喜欢哪一个!使用枚举的一个缺点是,每个检查枚举值的地方都需要一个 `match` 表达式或类似表达式来处理每个可能的变体。这可能会比这种 trait 对象解决方案更重复。
状态模式的权衡
我们已经展示了 Rust 能够实现面向对象的状态模式,以封装文章在每种状态下应具有的不同类型的行为。`Post` 上的方法对各种行为一无所知。我们组织代码的方式,我们只需要在一个地方查看就可以知道已发布文章可以表现出的不同方式:`Published` 结构体上 `State` trait 的实现。
如果我们创建个不使用状态模式的替代实现,我们可能会在 `Post` 的方法中,甚至在 `main` 代码中使用 `match` 表达式来检查文章的状态并在这些地方更改行为。这意味着我们将不得不查看多个地方才能理解文章处于已发布状态的所有含义!这将只会随着我们添加更多状态而增加:每个 `match` 表达式都需要另一个分支。
使用状态模式,`Post` 方法和我们使用 `Post` 的地方不需要 `match` 表达式,并且要添加新状态,我们只需要添加一个新的结构体并实现该结构体上的 trait 方法。
使用状态模式的实现很容易扩展以添加更多功能。要了解维护使用状态模式的代码的简单性,请尝试以下一些建议
- 添加一个 `reject` 方法,将文章的状态从 `PendingReview` 更改回 `Draft`。
- 需要两次调用 `approve` 才能将状态更改为 `Published`。
- 仅当文章处于 `Draft` 状态时才允许用户添加文本内容。提示:让状态对象负责内容可能发生的变化,但不负责修改 `Post`。
状态模式的一个缺点是,由于状态实现了状态之间的转换,因此某些状态彼此耦合。如果我们在 `PendingReview` 和 `Published` 之间添加另一个状态,例如 `Scheduled`,我们将不得不更改 `PendingReview` 中的代码以转换为 `Scheduled`。如果 `PendingReview` 不需要随着新状态的添加而更改,那将减少工作量,但这将意味着切换到另一种设计模式。
另一个缺点是我们重复了一些逻辑。为了消除一些重复,我们可能会尝试为 `State` trait 上的 `request_review` 和 `approve` 方法创建返回 `self` 的默认实现;但是,这将违反对象安全性,因为该 trait 不知道具体的 `self` 到底是什么。我们希望能够将 `State` 用作 trait 对象,因此我们需要其方法是对象安全的。
其他重复包括 `Post` 上的 `request_review` 和 `approve` 方法的类似实现。这两种方法都委托给 `Option` 的 `state` 字段中的值的相同方法的实现,并将 `state` 字段的新值设置为结果。如果我们在 `Post` 上有很多遵循此模式的方法,我们可能会考虑定义一个宏来消除重复(请参阅 “宏”第 20 章中的章节)。
通过完全按照面向对象语言的定义来实现状态模式,我们没有充分利用 Rust 的优势。让我们看看我们可以对 `blog` crate 进行哪些更改,以将无效状态和转换变为编译时错误。
将状态和行为编码为类型
我们将向您展示如何重新思考状态模式以获得一组不同的权衡。我们将状态编码为不同的类型,而不是完全封装状态和转换,使外部代码对其一无所知。因此,Rust 的类型检查系统将通过发出编译器错误来阻止尝试在仅允许已发布文章的地方使用草稿文章。
让我们考虑列表 17-11 中 `main` 的第一部分
文件名:src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
我们仍然允许使用 `Post::new` 创建草稿状态的新文章,并允许向文章内容添加文本。但是,我们不会在草稿文章上设置返回空字符串的 `content` 方法,而是使草稿文章根本不具有 `content` 方法。这样,如果我们尝试获取草稿文章的内容,我们将收到一个编译器错误,告诉我们该方法不存在。因此,我们将不可能在生产环境中意外显示草稿文章内容,因为该代码甚至无法编译。列表 17-19 显示了 `Post` 结构体和 `DraftPost` 结构体的定义,以及每个结构体上的方法
文件名:src/lib.rs
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);
}
}
列表 17-19:具有 `content` 方法的 `Post` 和没有 `content` 方法的 `DraftPost`
`Post` 和 `DraftPost` 结构体都具有一个私有的 `content` 字段,用于存储博客文章文本。这些结构体不再具有 `state` 字段,因为我们将状态的编码移动到结构体的类型中。`Post` 结构体将表示已发布的文章,并且它具有一个返回 `content` 的 `content` 方法。
我们仍然有一个 `Post::new` 函数,但它不是返回 `Post` 的实例,而是返回 `DraftPost` 的实例。由于 `content` 是私有的,并且没有任何函数返回 `Post`,因此现在无法创建 `Post` 的实例。
`DraftPost` 结构体具有 `add_text` 方法,因此我们可以像以前一样向 `content` 添加文本,但请注意,`DraftPost` 没有定义 `content` 方法!因此,现在程序确保所有文章都以草稿文章开始,并且草稿文章的内容不可用于显示。任何试图绕过这些约束的尝试都将导致编译器错误。
将转换实现为转换为不同类型
那么我们如何获得已发布的文章呢?我们想要强制执行草稿文章必须经过审核和批准才能发布的规则。处于待审核状态的文章仍不应显示任何内容。让我们通过添加另一个结构体 `PendingReviewPost` 来实现这些约束,在 `DraftPost` 上定义 `request_review` 方法以返回 `PendingReviewPost`,并在 `PendingReviewPost` 上定义 `approve` 方法以返回 `Post`,如列表 17-20 所示
文件名:src/lib.rs
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,
}
}
}
列表 17-20:通过在 `DraftPost` 上调用 `request_review` 创建的 `PendingReviewPost` 以及将 `PendingReviewPost` 转换为已发布的 `Post` 的 `approve` 方法
`request_review` 和 `approve` 方法获取 `self` 的所有权,从而消耗 `DraftPost` 和 `PendingReviewPost` 实例,并将它们分别转换为 `PendingReviewPost` 和已发布的 `Post`。这样,在我们对 `DraftPost` 实例调用 `request_review` 之后,就不会有任何残留的 `DraftPost` 实例,依此类推。`PendingReviewPost` 结构体上没有定义 `content` 方法,因此尝试读取其内容会导致编译器错误,就像 `DraftPost` 一样。因为获取已定义 `content` 方法的已发布 `Post` 实例的唯一方法是在 `PendingReviewPost` 上调用 `approve` 方法,而获取 `PendingReviewPost` 的唯一方法是在 `DraftPost` 上调用 `request_review` 方法,所以我们现在已将博客文章工作流程编码到类型系统中。
但是,我们也需要对 main
做一些小的修改。request_review
和 approve
方法返回的是新的实例,而不是修改它们被调用的结构体,所以我们需要添加更多的 let post =
阴影赋值来保存返回的实例。我们也不能再有关于 draft 和 pending review 状态的 post 内容是空字符串的断言,我们也不需要它们了:我们不能再编译试图使用这些状态的 post 内容的代码了。main
中更新后的代码如列表 17-21 所示
文件名:src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
列表 17-21:对 main
的修改,以使用博客文章工作流程的新实现
我们需要对 main
进行修改以重新赋值 post
,这意味着这个实现不再完全遵循面向对象的状态模式:状态之间的转换不再完全封装在 Post
实现中。然而,我们的收获是无效状态现在是不可能的,因为类型系统和编译时发生的类型检查!这确保了某些错误,例如显示未发布的 post 的内容,将在它们进入生产环境之前被发现。
尝试本节开头关于列表 17-21 之后的 blog
crate 的建议任务,看看你对这个版本的代码的设计有何看法。请注意,某些任务可能已在此设计中完成。
我们已经看到,即使 Rust 能够实现面向对象的设计模式,其他模式,例如将状态编码到类型系统中,在 Rust 中也是可用的。这些模式有不同的权衡。尽管你可能非常熟悉面向对象的模式,但重新思考问题以利用 Rust 的特性可以带来好处,例如在编译时防止某些错误。由于某些特性(如所有权),面向对象的模式在 Rust 中并不总是最佳解决方案,而面向对象语言没有这些特性。
总结
无论你在阅读本章后是否认为 Rust 是一种面向对象的语言,你现在都知道可以使用 trait 对象在 Rust 中获得一些面向对象的特性。动态分发可以为你的代码提供一定的灵活性,以换取一点运行时性能。你可以使用这种灵活性来实现面向对象的模式,这些模式可以帮助你代码的可维护性。Rust 还具有其他面向对象语言所没有的特性,如所有权。面向对象的模式不总是利用 Rust 优势的最佳方式,但它是一个可用的选项。
接下来,我们将看看模式,这是 Rust 的另一个特性,可以实现很大的灵活性。我们在本书中已经简要地看过它们,但还没有看到它们的全部功能。让我们开始吧!