实现面向对象的设计模式


状态模式是一种面向对象的设计模式。该模式的关键在于,我们定义了一个值内部可以具有的一组状态。状态由一组状态对象表示,值的行为根据其状态而变化。我们将通过一个博客文章结构体的示例,该结构体有一个字段来保存其状态,该字段将是来自集合 “draft”、“review” 或 “published” 的状态对象。


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


使用状态模式的优点是,当程序的业务需求发生变化时,我们不需要更改保存状态的值的代码或使用该值的代码。我们只需要更新其中一个 state 对象中的代码来更改其规则或添加更多 state 对象。


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


最终功能将如下所示:


  1. 博客文章开始时是一个空草稿。

  2. 草稿完成后,请求对帖子进行审核。

  3. 帖子获得批准后,它就会被发布。

  4. 只有已发布的博客文章才会返回要打印的内容,因此不会意外发布未经批准的文章。


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


示例 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! 以进行演示。一个很好的单元测试是断言 draft 博客文章从 content 方法返回一个空字符串,但我们不会为这个例子编写测试。


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


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


定义 post 并在 draft 状态下创建新实例


让我们开始实施该库吧!我们知道我们需要一个包含一些内容的 public Post 结构体,因此我们将从结构体的定义和关联的 public new 函数开始创建一个 Post 实例,如示例 17-12 所示。我们还会将私有的 State trait 的 state trait 定义 Post 的所有 state 对象的行为 必须有。


然后,Post 将在 Option<T> 中保存 Box<dyn State> 的 trait 对象 在名为 state 的私有字段中保存 state 对象。您将了解为什么 选项 <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 实例、State trait 和 Draft 结构


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


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


存储文章内容的文本


我们在示例 17-11 中看到,我们希望能够调用一个名为 add_text 并为其传递一个 &str,然后将其添加为博客文章的文本内容。我们将其作为一种方法实现,而不是公开内容 field 指定为 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 方法以将文本添加到帖子的内容


add_text 方法采用对 self 的可变引用,因为我们正在更改调用 add_textPost 实例。然后我们调用 push_str String in content 并传递 text 参数以添加到保存的内容。此行为不取决于帖子所处的状态,因此它不是状态模式的一部分。add_text 方法根本不与 state 字段交互,但它是我们希望支持的行为的一部分。


确保草稿帖子的内容为空


即使在我们调用了 add_text 并在我们的帖子中添加了一些内容之后,我们仍然希望 content 方法返回一个空字符串 slice,因为帖子仍然处于 draft 状态,如示例 17-11 的第 7 行所示。现在,让我们用最简单的方法来实现 content 方法,这将满足这个要求:总是返回一个空字符串 slice。一旦我们实现了更改文章状态的功能,以便可以发布它,我们将稍后更改此设置。目前,帖子只能处于 draft 状态,因此帖子内容应始终为空。示例 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 方法添加一个占位符实现,该方法始终返回空字符串 slice


使用这个添加的内容方法,示例 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:在 上实现 request_review 方法 PostState trait


我们给 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 方法需要获取 state 值的所有权。这是 Poststate 字段中的 Option 的位置 进来:我们调用 take 方法将 Some 值从 state 中取出 field 并在其位置保留一个 None,因为 Rust 不允许我们在结构体中有未填充的字段。这允许我们将 state 值从 发布而不是借用它。然后我们将帖子的 state 值设置为此作的结果。


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


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


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


我们将 Post 上的 content 方法保持原样,返回一个空字符串 slice。现在,我们可以在 PendingReview 状态以及 Draft 状态,但我们希望在 PendingReview 状态中具有相同的行为。示例 17-11 现在可以工作到第 10 行了!


添加 approve 以更改内容的行为


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:在 PostState trait


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


request_review PendingReview 的工作方式类似,如果我们调用 approve 方法,则它不会生效,因为 approve 将返回 self。当我们对 PendingReview 调用 approve 时,它会返回 Published 结构的新装箱实例。Published 结构实现 State trait 中,对于 request_review 方法和 approve 方法,它会返回自身,因为帖子应该保留在 Published 在这些情况下。


现在我们需要更新 Post 上的 content 方法。我们希望从 content 返回的值取决于 Post 的当前状态,因此我们将 Post 委托给在其状态上定义的 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 方法,并将 post 实例(即 self)作为参数传递。然后,我们返回对 state 值使用 content 方法返回的值。


我们在 Option 上调用 as_ref 方法,因为我们想要对 Option 内部值的引用,而不是值的所有权。由于 state选项<Box<dyn State>>,当我们as_ref调用时,将返回一个选项<&Box<dyn State>>。如果我们没有调用 as_ref,我们会得到一个错误,因为我们无法将 state 移出函数参数的 borrowed &self


然后我们调用 unwrap 方法,我们知道它永远不会 panic,因为我们知道 Post 上的方法确保 state 始终包含一个 Some 值。这是我们讨论的案例之一 “您拥有的信息比 Compiler“部分,当我们知道 None 值永远不可能时,即使编译器无法理解这一点。


此时,当我们在 &Box<dyn State> 上调用 content 时,解引用强制转换将对 &Box 生效,因此 content 方法最终将在实现 State trait 的类型上调用。这意味着我们需要将内容添加到 State trait 定义中,这就是我们将根据我们拥有的 state 返回什么内容的逻辑的地方,如示例 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 中 特性


我们为返回空字符串 slice 的 content 方法添加一个默认实现。这意味着我们不需要在 Draft 上实施内容PendingReview 结构。Published 结构将覆盖内容 方法并返回 post.content 中的值。


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


我们完成了 — 示例 17-11 现在都起作用了!我们已经使用博客文章工作流的规则实现了状态模式。与规则相关的逻辑存在于 state 对象中,而不是分散在整个 Post 中。


为什么不是 Enum?


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


状态模式的权衡


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


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


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


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


  • 添加一个 reject 方法,将文章的状态从 PendingReview 改回 Draft

  • 需要两次调用才能批准,然后才能将状态更改为 Published (已发布)。

  • 允许用户仅在文章处于 Draft (草稿) 状态时添加文本内容。提示:让 state 对象负责内容可能发生的变化,但不负责修改 Post


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


另一个缺点是我们复制了一些逻辑。要消除一些 复制,我们可能会尝试为 request_review批准 State trait 上返回 self 的方法;然而,这将违反 Object Safety,因为 trait 不知道具体的 self 到底是什么。我们希望能够将 State 用作 trait 对象,因此我们需要它的方法是对象安全的。


其他重复包括 request_review 的类似实现 和 approve methods on Post.两种方法都委托对 Optionstate 字段中的值执行相同的方法,并将 state 字段的新值设置为 result。如果我们在 Post 上有很多方法 遵循此模式,我们可能会考虑定义一个宏来消除 repetition (参见第 19 章的 “Macros” 部分)。


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


将状态和行为编码为类型


我们将向您展示如何重新考虑状态模式以获得一组不同的权衡。我们将 state 编码为不同的类型,而不是完全封装 state 和 transition,以便外部代码不知道它们。因此,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 在 draft 状态下创建新帖子 以及向帖子内容添加文本的能力。但是,与其拥有 content 方法,那么 Draft Post 就完全没有 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 方法和 没有内容方法的 DraftPost


PostDraftPost 结构体都有一个私有内容字段,用于存储博客文章文本。结构体不再具有 state 字段,因为我们正在将 state 的编码移动到结构体的类型。邮报 struct 将表示已发布的帖子,并且它有一个返回内容的 content 方法。


我们仍然有一个 Post::new 函数,但不是返回一个 Post 时,它会返回 DraftPost 的实例。由于内容是私有的,并且没有任何返回 Post 的函数,因此现在无法创建 Post 的实例。


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


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


那么我们如何获得已发布的帖子呢?我们希望强制执行这样一条规则,即草稿帖子必须先经过审核和批准,然后才能发布。处于 pending review 状态的帖子仍不应显示任何内容。让我们通过添加另一个结构 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 转换为已发布的帖子


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


但是我们也必须对 main 进行一些小的更改。request_reviewapprove 方法返回新实例,而不是修改调用它们的 struct,因此我们需要添加更多 let post = shadowing assignments 来保存返回的实例。我们也不能让关于 draft 和 pending review 帖子内容的断言为空字符串,我们也不需要它们:我们不能再编译试图使用这些状态的帖子内容的代码。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,这意味着这个实现不再完全遵循面向对象的 state 模式:state 之间的转换不再完全封装在 Post 实现中。然而,我们的好处是,由于类型系统和编译时发生的类型检查,无效状态现在是不可能的!这可确保在将某些错误(如显示未发布的帖子的内容)投入生产之前被发现。


尝试本节开头在 blog crate 上建议的任务,就像示例 17-21 之后一样,看看你对这个版本代码的设计有什么看法。请注意,某些任务可能已经在此设计中完成。


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


总结


不管你看完本章后是否认为 Rust 是一种面向对象语言,你现在知道你可以使用 trait objects 来获得 Rust 中的一些面向对象功能。动态调度可以为您的代码提供一些灵活性,以换取一点运行时性能。您可以利用这种灵活性来实现面向对象的模式,这有助于提高代码的可维护性。Rust 还具有面向对象语言所没有的其他功能,例如所有权。面向对象的模式并不总是利用 Rust 优势的最佳方式,但是一个可用的选项。


接下来,我们将看看模式,这是 Rust 的另一个功能,可以带来很大的灵活性。我们在整本书中简要地研究了它们,但还没有看到它们的全部功能。我们走吧!