实现面向对象的设计模式

状态模式 是一种面向对象的设计模式。该模式的核心在于我们定义了一个值可以内部拥有一组状态。这些状态由一组 状态对象 表示,值的行为会根据其状态而变化。我们将通过一个博客文章结构体的例子来说明,该结构体有一个字段用于保存其状态,该状态将是从“草稿”、“审核”或“已发布”这组状态对象中选择的一个。

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

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

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

最终的功能将如下所示:

  1. 一篇博客文章始于一个空白草稿。
  2. 当草稿完成后,请求对帖子进行审查。
  3. 当帖子被批准后,它就会发布。
  4. 只有已发布的博客文章才会返回内容以供打印,因此未获批准的文章不会意外发布。

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

列表 18-11 以代码形式展示了此工作流程:这是我们将要在名为 blog 的库 crate 中实现的 API 的示例用法。这还不能编译,因为我们还没有实现 blog crate。

Filename: 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());
}
Listing 18-11: Code that demonstrates the desired behavior we want our blog crate to have

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

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

请注意,我们从 crate 中交互的唯一类型是 Post 类型。这种类型将使用状态模式,并将持有一个值,该值将是表示帖子可以处于的三种状态之一的状态对象——草稿、等待审核或已发布。从一个状态转换到另一个状态将由 Post 类型内部管理。状态的变化是响应库用户在 Post 实例上调用的方法,但他们不必直接管理状态变化。此外,用户不会在状态上出错,例如在未审核之前发布帖子。

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

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

然后 Post 将在名为 state 的私有字段中持有 Box<dyn State> 特性对象,该对象位于 Option<T> 内部。你稍后会看到为什么需要 Option<T>

Filename: 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 {}
Listing 18-12: Definition of a Post struct and a new function that creates a new Post instance, a State trait, and a Draft struct

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

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

存储帖子内容的文本

我们在清单 18-11 中看到,我们希望能够调用一个名为 add_text 的方法,并传递一个 &str,然后将其作为博客文章的文本内容添加。我们将其实现为一个方法,而不是将 content 字段公开为 pub,这样我们可以在以后实现一个控制 content 字段数据读取方式的方法。add_text 方法相当简单,因此让我们在 impl Post 块中添加清单 18-13 中的实现:

Filename: 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 {}
Listing 18-13: Implementing the add_text method to add text to a post’s content

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

确保草稿帖子的内容为空

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

Filename: 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 {}
Listing 18-14: Adding a placeholder implementation for the content method on Post that always returns an empty string slice

有了这个添加的 content 方法,清单 18-11 中直到第 7 行的所有内容都按预期工作。

请求审查帖子会改变其状态

接下来,我们需要添加请求审查帖子的功能,这应该将其状态从Draft更改为PendingReview。清单18-15显示了这段代码:

Filename: 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
    }
}
Listing 18-15: Implementing request_review methods on Post and the State trait

我们给 Post 提供一个名为 request_review 的公共方法,该方法将接受一个可变的 self 引用。然后我们在 Post 的当前状态上调用一个内部的 request_review 方法,这个第二个 request_review 方法会消耗当前状态并返回一个新状态。

我们向 State 特性添加了 request_review 方法;所有实现该特性的类型现在都需要实现 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值的所有权。这确保在我们将旧的state值转换为新状态后,Post不能使用旧的state值。

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

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

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

添加 approve 以更改 content 的行为

approve 方法将类似于 request_review 方法:它将把 state 设置为当前状态在被批准时应具有的值,如清单 18-16 所示:

Filename: 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
    }
}
Listing 18-16: Implementing the approve method on Post and the State trait

我们向 State 特性添加 approve 方法,并添加一个新的结构体来实现 State,即 Published 状态。

类似于 PendingReview 上的 request_review 方法,如果我们在一个 Draft 上调用 approve 方法,它将不会产生任何效果,因为 approve 会返回 self。当我们在一个 PendingReview 上调用 approve 时,它会返回一个新的、装箱的 Published 结构体实例。Published 结构体实现了 State 特性,对于 request_review 方法和 approve 方法,它都会返回自身,因为在这些情况下,帖子应该保持在 Published 状态。

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

Filename: 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
    }
}
Listing 18-17: Updating the content method on Post to delegate to a content method on State

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

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

我们然后调用 unwrap 方法,我们知道它永远不会 panic,因为我们知道 Post 上的方法确保 state 在这些方法执行完毕时总是包含一个 Some 值。这是我们在 “你比编译器有更多的信息的情况” 一节中讨论的情况之一,即我们知道 None 值是不可能的,即使编译器无法理解这一点。

在这一点上,当我们对 &Box<dyn State> 调用 content 时,解引用强制会作用于 &Box,因此 content 方法最终会被调用在实现 State 特性的类型上。这意味着我们需要将 content 添加到 State 特性定义中,这就是我们将根据所处状态决定返回什么内容的逻辑放置的地方,如清单 18-18 所示:

Filename: 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
    }
}
Listing 18-18: Adding the content method to the State trait

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

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

而且我们完成了—清单 18-11 现在全部可以工作了!我们已经使用博客文章工作流的规则实现了状态模式。与规则相关的逻辑存在于状态对象中,而不是分散在 Post 中。

Why Not An Enum?

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

状态模式的权衡

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

如果我们创建一个不使用状态模式的替代实现,我们可能会在 Post 的方法中或甚至在检查帖子状态并改变这些地方行为的 main 代码中使用 match 表达式。这意味着我们必须查看多个地方才能理解帖子处于已发布状态的所有影响!随着我们添加更多状态,这种情况只会增加:每个 match 表达式都需要另一个分支。

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

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

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

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

另一个缺点是我们复制了一些逻辑。为了减少一些重复,我们可能会尝试为 State 特性上的 request_reviewapprove 方法提供默认实现,这些方法返回 self;然而,这将违反对象安全性,因为特性不知道具体的 self 会是什么。我们希望能够将 State 用作特性对象,因此需要其方法是对象安全的。

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

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

Encoding States and Behavior as Types

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

让我们考虑一下清单 18-11 中 main 的第一部分:

Filename: 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 结构体的定义,以及每个结构体上的方法:

Filename: 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);
    }
}
Listing 18-19: A Post with a content method and DraftPost without a content method

PostDraftPost 结构体都有一个私有的 content 字段,用于存储博客文章的文本。这些结构体不再有 state 字段,因为我们正在将状态的编码转移到结构体的类型上。Post 结构体将表示已发布的文章,并且它有一个 content 方法,用于返回 content

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

DraftPost 结构体有一个 add_text 方法,因此我们可以像以前一样向 content 添加文本,但请注意,DraftPost 没有定义 content 方法!因此,现在程序确保所有帖子都以草稿形式开始,而草稿帖子的内容无法显示。任何试图绕过这些限制的行为都会导致编译错误。

Implementing Transitions as Transformations into Different Types

那么我们如何获得一个已发布的帖子呢?我们希望强制执行这样的规则:草稿帖子必须经过审核和批准后才能发布。处于待审核状态的帖子仍然不应显示任何内容。让我们通过添加另一个结构体PendingReviewPost,在DraftPost上定义request_review方法以返回一个PendingReviewPost,并在PendingReviewPost上定义一个approve方法以返回一个Post,如清单18-20所示:

Filename: 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,
        }
    }
}
Listing 18-20: A PendingReviewPost that gets created by calling request_review on DraftPost and an approve method that turns a PendingReviewPost into a published Post

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

但我们也需要对 main 做一些小的修改。request_reviewapprove 方法返回新的实例而不是修改它们被调用的结构体,因此我们需要添加更多的 let post = 阴影赋值来保存返回的实例。我们也不能再有关于草稿和待审帖子内容为空字符串的断言,我们也不需要它们:我们不能再编译尝试使用这些状态帖子内容的代码。更新后的 main 代码如清单 18-21 所示:

Filename: 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());
}
Listing 18-21: Modifications to main to use the new implementation of the blog post workflow

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

尝试本节开头建议的任务,针对列表 18-21 之后的 blog crate,看看你对这个版本代码的设计有什么看法。请注意,某些任务在这个设计中可能已经完成。

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

摘要

无论你在阅读本章后是否认为 Rust 是一种面向对象的语言,你现在知道你可以使用特质对象在 Rust 中获得一些面向对象的特性。动态分发可以让你的代码在牺牲一些运行时性能的情况下获得一定的灵活性。你可以利用这种灵活性来实现有助于代码可维护性的面向对象模式。Rust 还具有其他特性,如所有权,这是面向对象语言所不具备的。面向对象的模式并不总是利用 Rust 特性优势的最佳方式,但确实是一个可用的选项。

接下来,我们将研究模式,这是 Rust 的另一个特性,它提供了很大的灵活性。我们在书中简要地看过它们,但还没有看到它们的全部功能。让我们开始吧!