实现面向对象的设计模式

状态模式是一种面向对象的设计模式。该模式的关键在于我们定义了一个值可以在内部拥有的一组状态。这些状态由一组状态对象表示,并且值的行为会根据其状态而改变。我们将通过一个博客文章结构体的示例进行讲解,该结构体有一个字段来保存其状态,该状态将是“草稿”、“审核中”或“已发布”集合中的一个状态对象。

状态对象共享功能:当然,在 Rust 中,我们使用结构体和 trait 而不是对象和继承。每个状态对象负责其自身的行为,并负责管理何时应更改为另一种状态。保存状态值的对象对状态的不同行为或何时在状态之间转换一无所知。

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

首先,我们将以更传统的面向对象的方式实现状态模式,然后我们将使用在 Rust 中更自然的方法。让我们深入研究如何逐步使用状态模式实现博客文章工作流。

最终的功能将如下所示

  1. 一篇博文最初是一个空的草稿。
  2. 当草稿完成后,将请求对帖子进行审核。
  3. 当帖子获得批准后,它将被发布。
  4. 只有已发布的博文才会返回要打印的内容,因此未经批准的帖子不会意外发布。

对帖子进行的任何其他更改都应无效。例如,如果我们尝试在请求审核之前批准一篇草稿博文,则该帖子应保持未发布的草稿状态。

列表 18-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()); }
列表 18-11:演示我们希望 blog crate 具有的所需行为的代码

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

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

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

定义 Post 并在草稿状态下创建新实例

让我们开始实现库!我们知道我们需要一个公共的 Post 结构体来保存一些内容,因此我们将从结构体的定义和一个关联的公共 new 函数开始,以创建 Post 的实例,如列表 18-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 {}
列表 18-12:Post 结构体和创建新的 Post 实例的 new 函数、State trait 和 Draft 结构体的定义

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

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

存储帖子内容的文本

我们在列表 18-11 中看到,我们希望能够调用一个名为 add_text 的方法,并将其传递一个 &str,然后将其添加为博文的文本内容。我们将此实现为一个方法,而不是将 content 字段公开为 pub,以便稍后我们可以实现一个方法来控制如何读取 content 字段的数据。add_text 方法非常简单,因此让我们将列表 18-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 {}
列表 18-13:实现 add_text 方法以将文本添加到帖子的 content

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

确保草稿帖子的内容为空

即使在我们调用 add_text 并向帖子添加了一些内容之后,我们仍然希望 content 方法返回一个空字符串切片,因为该帖子仍处于草稿状态,如列表 18-11 的第 7 行所示。目前,让我们使用最简单的方法来实现 content 方法,这将满足此要求:始终返回一个空字符串切片。我们稍后将更改此设置,一旦我们实现了更改帖子状态使其可以发布的功能。到目前为止,帖子只能处于草稿状态,因此帖子内容应始终为空。列表 18-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 {}
列表 18-14:在 Post 上添加 content 方法的占位符实现,该方法始终返回一个空字符串切片

通过添加的 content 方法,列表 18-11 中直到第 7 行的所有内容都可以按预期工作。

请求审核帖子会更改其状态

接下来,我们需要添加请求审核帖子的功能,这将使其状态从 Draft 更改为 PendingReview。列表 18-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 } }
列表 18-15:在 PostState 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 方法需要取得状态值的所有权。这就是 Poststate 字段中的 Option 的用武之地:我们调用 take 方法从 state 字段中取出 Some 值,并在其位置留下 None,因为 Rust 不允许我们在结构体中拥有未填充的字段。这使我们可以将 state 值移出 Post 而不是借用它。然后,我们将帖子的 state 值设置为此操作的结果。

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

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

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

我们将 Post 上的 content 方法保持原样,返回一个空字符串切片。我们现在可以使 Post 处于 PendingReview 状态以及 Draft 状态,但我们希望在 PendingReview 状态下具有相同的行为。列表 18-11 现在可以工作到第 10 行!

添加 approve 以更改 content 的行为

approve 方法将类似于 request_review 方法:它会将 state 设置为当前状态在获得批准时应具有的值,如列表 18-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 } }
列表 18-16:在 PostState trait 上实现 approve 方法

我们将 approve 方法添加到 State trait,并添加一个新的实现 State 的结构体,即 Published 状态。

类似于 PendingReview 上的 request_review 的工作方式,如果我们在 Draft 上调用 approve 方法,它将不起作用,因为 approve 将返回 self。当我们在 PendingReview 上调用 approve 时,它会返回一个新的、装箱的 Published 结构体的实例。Published 结构体实现了 State trait,并且对于 request_review 方法和 approve 方法,它都会返回自身,因为在这种情况下,帖子应保持 Published 状态。

现在我们需要更新 Post 上的 content 方法。我们希望从 content 返回的值取决于 Post 的当前状态,因此我们将让 Post 委托给在其 state 上定义的 content 方法,如列表 18-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 } }
列表 18-17:更新 Post 上的 content 方法以委托给 State 上的 content 方法

由于目标是将所有这些规则都保留在实现 State 的结构体内部,因此我们在 state 中的值上调用 content 方法,并将帖子实例(即 self)作为参数传递。然后,我们返回从在 state 值上使用 content 方法返回的值。

我们在 Option 上调用 as_ref 方法,因为我们想要引用 Option 内部的值,而不是拥有该值的所有权。由于 stateOption<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 定义中,这将是我们放置逻辑的地方,该逻辑用于根据我们拥有的状态返回哪些内容,如列表 18-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 } }
列表 18-18:将 content 方法添加到 State trait

我们为 content 方法添加了一个默认实现,该方法返回一个空字符串切片。这意味着我们不需要在 DraftPendingReview 结构体上实现 contentPublished 结构体将覆盖 content 方法并返回 post.content 中的值。

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

我们完成了——列表 18-11 中的所有内容现在都可以工作了!我们已经使用博文工作流程的规则实现了状态模式。与规则相关的逻辑存在于状态对象中,而不是分散在整个 Post 中。

为什么不是枚举?

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

状态模式的权衡

我们已经表明,Rust 能够实现面向对象的状态模式,以封装帖子在每种状态下应具有的不同类型的行为。Post 上的方法对各种行为一无所知。我们组织代码的方式是,我们只需要在一个地方查看即可了解已发布的帖子可以表现出的不同方式:Published 结构体上 State trait 的实现。

如果我们创建一个不使用状态模式的替代实现,我们可能会在 Post 上的方法中甚至在 main 代码中使用 match 表达式,这些表达式检查帖子的状态并更改这些位置的行为。这意味着我们将不得不查看多个地方才能了解帖子处于已发布状态的所有含义!这将仅在我们添加更多状态时增加:每个 match 表达式都需要另一个分支。

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

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

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

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

另一个缺点是我们重复了一些逻辑。为了消除一些重复,我们可以尝试为 State trait 上的 request_reviewapprove 方法创建默认实现,这些方法返回 self;但是,这将与 dyn 不兼容,因为 trait 不知道具体的 self 到底是什么。我们希望能够将 State 用作 trait 对象,因此我们需要其方法与 dyn 兼容。

其他重复项包括 Postrequest_reviewapprove 方法的类似实现。这两种方法都委托给 Optionstate 字段中的值的同一方法的实现,并将 state 字段的新值设置为结果。如果我们在 Post 上有很多遵循此模式的方法,我们可能会考虑定义一个宏来消除重复(请参阅 “宏”第 20 章中的章节)。

通过完全按照面向对象语言的定义实现状态模式,我们没有充分利用 Rust 的优势。让我们看看我们可以对 blog crate 进行的一些更改,这些更改可以将无效状态和转换变为编译时错误。

将状态和行为编码为类型

我们将向您展示如何重新思考状态模式以获得一组不同的权衡。与其完全封装状态和转换,使外部代码对其一无所知,不如将状态编码为不同的类型。因此,Rust 的类型检查系统将通过发出编译器错误来阻止尝试在只允许已发布帖子的位置使用草稿帖子。

让我们考虑列表 18-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 方法。这样,如果我们尝试获取草稿帖子的内容,我们将收到一个编译器错误,告诉我们该方法不存在。因此,我们将不可能在生产环境中意外显示草稿帖子内容,因为该代码甚至无法编译。列表 18-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); } }
列表 18-19:具有 content 方法的 Post 和没有 content 方法的 DraftPost

PostDraftPost 结构体都有一个私有的 content 字段,用于存储博文文本。结构体不再具有 state 字段,因为我们将状态的编码移动到结构体的类型中。Post 结构体将表示已发布的帖子,并且它有一个返回 contentcontent 方法。

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

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

将转换实现为不同类型的转换

那么我们如何获得已发布的帖子呢?我们想强制执行规则,即草稿帖子必须经过审核和批准后才能发布。处于待审核状态的帖子仍不应显示任何内容。让我们通过添加另一个结构体 PendingReviewPost 来实现这些约束,在 DraftPost 上定义 request_review 方法以返回 PendingReviewPost,并在 PendingReviewPost 上定义 approve 方法以返回 Post,如列表 18-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, } } }
列表 18-20:通过在 DraftPost 上调用 request_review 创建的 PendingReviewPost 和将 PendingReviewPost 转换为已发布的 Postapprove 方法

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

但是我们还必须对 main 进行一些小的更改。request_reviewapprove 方法返回新实例,而不是修改它们被调用的结构体,因此我们需要添加更多 let post = 阴影赋值来保存返回的实例。我们也不能再让关于草稿和待审核帖子内容的断言为空字符串,我们也不需要它们:我们无法编译尝试使用这些状态下帖子的内容的代码。main 中的更新代码如列表 18-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()); }
列表 18-21:对 main 的修改,以使用博文工作流程的新实现

我们需要对 main 进行重新分配 post 的更改,这意味着此实现不再完全遵循面向对象的状态模式:状态之间的转换不再完全封装在 Post 实现中。但是,我们的收获是,由于类型系统以及在编译时发生的类型检查,无效状态现在是不可能的!这确保了某些错误(例如未发布帖子的内容的显示)将在它们进入生产环境之前被发现。

在列表 18-21 之后的 blog crate 上尝试本节开头建议的任务,以了解您对代码的此版本的设计有何看法。请注意,某些任务可能已在此设计中完成。

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

总结

无论您在阅读本章后是否认为 Rust 是一种面向对象的语言,您现在都知道可以使用 trait 对象在 Rust 中获得一些面向对象的功能。动态分发可以为您的代码提供一定的灵活性,以换取少许运行时性能。您可以使用此灵活性来实现面向对象的模式,这些模式可以帮助您代码的可维护性。Rust 还具有其他功能(如所有权),这些功能是面向对象语言所没有的。面向对象的模式并不总是充分利用 Rust 优势的最佳方式,但它是一种可用的选项。

接下来,我们将研究模式,模式是 Rust 的另一个功能,可实现许多灵活性。我们在整本书中都简要地介绍了它们,但尚未了解它们的全部功能。让我们开始吧!