实现面向对象的设计模式

状态模式是一种面向对象的设计模式。关键 pattern 的含义是我们定义了一个值内部可以具有的一组状态。这 状态由一组状态对象表示,值的行为 根据其状态进行更改。我们将通过一个博客示例 post 结构体,该结构体具有一个用于保存其状态的字段,该 state 对象 从设置“草稿”、“审阅”或“已发布”。

state 对象共享功能:当然,在 Rust 中,我们使用 structs 和 traits 而不是 objects 和 inheritance。每个 state 对象都负责 为了它自己的行为,并控制它何时应该变成另一个 州。保存 state 对象的值对不同的 状态的行为或何时在状态之间转换。

使用状态模式的优点是,当业务 要求更改,则不需要更改 value 来保存状态或使用该值的代码。我们只需要 更新其中一个 State 对象中的代码以更改其规则,或者 添加更多 State 对象。

首先,我们将以更传统的 面向对象的方式,那么我们将使用一种更自然的方法 锈。让我们深入研究如何使用 state 模式。

最终功能将如下所示:

  1. 博客文章开始时是一个空草稿。
  2. 草稿完成后,请求对帖子进行审核。
  3. 帖子获得批准后,它就会被发布。
  4. 只有已发布的博客文章才会返回内容进行打印,因此未经批准的文章无法返回 意外发布。

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

示例 17-11 以代码形式展示了这个工作流:这是 API 中,我们将在名为 .这还不会编译 因为我们还没有实现 crate。blogblog

文件名: 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 仍为 Draft。我们添加了用于演示的代码 目的。一个很好的单元测试是断言草稿博客 post 从该方法返回一个空字符串,但我们不会 为此示例编写测试。Post::newassert_eq!content

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

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

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

让我们开始实施该库吧!我们知道我们需要一个 public 结构体,因此我们将从 结构的定义和关联的公共函数来创建 实例 ,如示例 17-12 所示。我们还将创建一个私有 trait,它将定义 a 的所有 state 对象必须具有的行为。PostnewPostStatePost

then 将保存一个 trait 对象 of 在一个名为 state 对象的私有字段中。你很快就会明白为什么 this 是必要的。PostBox<dyn State>Option<T>stateOption<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 结构体的新函数

trait 定义了不同帖子状态共享的行为。这 state 对象为 、 和 ,它们都将 实现 trait。目前,trait 没有任何方法,并且 我们首先只定义 State,因为这是我们 想要一个帖子开始。StateDraftPendingReviewPublishedStateDraft

当我们创建一个新 时,我们将其字段设置为一个值 包含 .这指向结构的新实例。 这确保了每当我们创建一个新的 实例时,它都会以 草稿。因为 的字段是私有的,所以没有办法 在任何其他状态中创建 A!在该函数中,我们将字段设置为新的空 .PoststateSomeBoxBoxDraftPoststatePostPostPost::newcontentString

存储文章内容的文本

在示例 17-11 中,我们看到我们希望能够调用一个名为 name 的方法,并传递给它,然后将其添加为 博客文章。我们将其实现为方法,而不是将字段公开为 ,以便稍后我们可以实现一个控制如何 读取字段的数据。方法很漂亮 很简单,所以让我们将示例 17-13 中的实现添加到块中:add_text&strcontentpubcontentadd_textimpl 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 方法以添加 文本添加到帖子内容

该方法采用对 的可变引用,因为我们是 更改我们正在调用的实例。然后我们调用 in 并将参数传递给 add to 保存的 .此行为不取决于帖子所处的状态, 所以它不是 state 模式的一部分。该方法不交互 替换为字段,但这是我们想要的行为的一部分 支持。add_textselfPostadd_textpush_strStringcontenttextcontentadd_textstate

确保草稿帖子的内容为空

即使在我们打电话并在我们的帖子中添加了一些内容之后,我们仍然 希望该方法返回一个空字符串 slice,因为 POST 是 仍然处于 draft 状态,如示例 17-11 的第 7 行所示。现在,让我们 使用最简单的东西实现方法,这将实现此 要求:始终返回空字符串 slice。我们稍后会更改此设置 一旦我们实现了更改帖子状态以便可以发布的功能。 到目前为止,帖子只能处于 draft 状态,因此 post 内容应始终 为空。示例 17-14 显示了这个占位符实现:add_textcontentcontent

文件名: 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 上始终返回空字符串 slice 的 content 方法

使用这个添加的方法,示例 17-11 中的所有内容到第 7 行 按预期工作。content

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

接下来,我们需要添加请求对帖子进行审核的功能,这应该 将其状态从 更改为 。示例 17-15 显示了这段代码:DraftPendingReview

文件名: 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:在 PostState trait 上实现 request_review 方法

我们给出一个名为 引用 .然后我们在 的当前状态,第二个方法使用 current state 并返回一个新 state。Postrequest_reviewselfrequest_reviewPostrequest_review

我们将方法添加到 trait 中;所有类型的 implement the trait 现在需要实现该方法。 请注意,不要将 、 或 作为第一个 参数,我们有 。此语法表示 method 仅在对持有 type 的 a 调用时有效。此语法采用 的所有权,使旧状态无效,以便 的 state 值可以转换为新状态。request_reviewStaterequest_reviewself&self&mut selfself: Box<Self>BoxBox<Self>Post

要使用旧状态,该方法需要获得所有权 的状态值。这就是 in the field of 的用武之地:我们调用该方法将值从字段中取出,并在其位置上留下 a,因为 Rust 不允许我们使用 结构中未填充的字段。这让我们将值移出而不是借用它。然后我们将帖子的值设置为 此作的结果。request_reviewOptionstatePosttakeSomestateNonestatePoststate

我们需要临时设置,而不是直接设置 使用类似代码来获取 值。这可确保在 我们已将其转变为新状态。stateNoneself.state = self.state.request_review();statePoststate

方法 on 返回 new 结构体的一个新的装箱实例,该实例表示 post 等待 回顾。该结构还实现了 但不执行任何转换。相反,它会返回自身,因为当我们 请求对已在该州的帖子进行审核,则应保留 在该州。request_reviewDraftPendingReviewPendingReviewrequest_reviewPendingReviewPendingReview

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

我们将方法保持原样,返回一个空字符串 片。我们现在可以在 state 和 state 中都有 a,但我们希望在 state 中具有相同的行为。 示例 17-11 现在可以工作到第 10 行了!contentPostPostPendingReviewDraftPendingReview

添加 approve 以更改内容的行为

该方法将类似于该方法:它将 设置为 Current State 表示它应该具有的值 state 被批准,如示例 17-16 所示:approverequest_reviewstate

文件名: 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 方法

我们将方法添加到 trait 中,并添加一个新的结构体,该结构体 实施,状态。approveStateStatePublished

类似于 on 的工作方式,如果我们在 a 上调用该方法,它不会有任何效果,因为会 返回。当我们调用 时,它会返回一个新的 结构的 boxed 实例。结构体实现了 trait,对于方法和方法,它都返回自身,因为在这些情况下,帖子应该保持状态。request_reviewPendingReviewapproveDraftapproveselfapprovePendingReviewPublishedPublishedStaterequest_reviewapprovePublished

现在我们需要更新 上的方法。我们想要价值 return from to 取决于 的当前状态,因此我们是 将委托给在其 , 如示例 17-17 所示:contentPostcontentPostPostcontentstate

文件名: 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的内容方法

因为目标是将所有这些规则都保留在实现 的结构体中,所以我们在 中的值上调用一个方法,并将 post instance(即 )作为参数。然后我们返回 通过对 value 使用 method 返回。Statecontentstateselfcontentstate

我们在 上调用该方法,因为我们想要对 value 内的 而不是 ownership 的 value。因为 是 ,所以当我们调用 时,会返回 an。如果我们没有调用 ,我们会得到一个错误,因为 我们无法移出 function 参数的借用。as_refOptionOptionstateOption<Box<dyn State>>as_refOption<&Box<dyn State>>as_refstate&self

然后我们调用该方法,我们知道它永远不会 panic,因为我们 了解确保的方法,当这些方法完成后,它将始终包含一个值。这是我们讨论的案例之一 “您拥有的信息比 Compiler“部分的 知道值永远不可能,即使编译器无法 来理解这一点。unwrapPoststateSomeNone

此时,当我们调用 , deref 强制 将对 和 因此该方法将 最终在实现 trait 的类型上调用。这意味着 我们需要添加到 trait 定义中,这就是 我们将根据 have,如示例 17-18 所示:content&Box<dyn State>&BoxcontentStatecontentState

文件名: 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 中

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

请注意,我们需要对此方法进行生命周期注释,正如我们在 第 10 章.我们将对 a 的引用作为参数,并返回一个 引用到该部分,因此返回的引用的生命周期为 与参数的生存期相关。postpostpost

我们完成了 — 示例 17-11 现在都起作用了!我们已经实施了 state pattern 替换为 blog post 工作流的规则。与 rules 存在于 state 对象中,而不是分散在整个 .Post

为什么不是 Enum?

您可能一直想知道为什么我们不使用 an 和不同的 可能的 POST 状态作为变体。这当然是一个可能的解决方案,试试 它并比较最终结果,看看你更喜欢哪个!一个缺点 使用 enum 是每个检查 enum 值的地方都需要一个 expression 或类似的东西来处理每个可能的变体。这可以 获得比此 trait object 解决方案更重复的 get。enummatch

状态模式的权衡

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

如果我们要创建一个不使用 state 的替代实现 pattern,我们可以改用 on 或 甚至在检查 POST 状态和更改行为的代码中 在那些地方。这意味着我们将不得不在几个地方寻找 了解帖子处于 published 状态的所有含义!这 只会增加我们添加的 states 越多:每个表达式 需要另一只手臂。matchPostmainmatch

使用 state 模式,我们使用的方法和位置不会 需要表达式,并且要添加新状态,我们只需要添加一个 new struct 并在该结构体上实现 trait 方法。PostPostmatch

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

  • 添加一个从 back 更改帖子状态的方法 自。rejectPendingReviewDraft
  • 需要两次调用 to 才能将状态更改为 。approvePublished
  • 允许用户仅在帖子处于状态时添加文本内容。 提示:让 state 对象负责 content 但不负责修改 .DraftPost

状态模式的一个缺点是,因为状态实现了 状态之间的转换,一些状态是相互耦合的。如果我们 在 和 之间添加另一个状态,例如 , 我们必须将代码更改为 transition to instead。如果不需要,工作量会更少 更改,但这意味着切换到 另一种设计模式。PendingReviewPublishedScheduledPendingReviewScheduledPendingReview

另一个缺点是我们复制了一些逻辑。要消除一些 duplication 时,我们可能会尝试对返回 ; 但是,这会违反对象安全,因为 trait 不知道什么 具体将是 Eactly。我们希望能够用作 trait 对象,因此我们需要它的方法对对象是安全的。request_reviewapproveStateselfselfState

其他重复包括 上 和 方法的类似实现。这两种方法都委托给 对字段中的值执行相同的方法,并设置新的 值添加到结果中。如果我们有很多方法都遵循这种模式,我们可能会考虑定义一个宏来消除 repetition (参见第 19 章的 “Macros” 部分)。request_reviewapprovePoststateOptionstatePost

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

将状态和行为编码为类型

我们将向您展示如何重新考虑状态模式以获得一组不同的 权衡取舍。而不是完全封装状态和转换,所以 外部代码不知道它们,我们会将 state 编码为不同的 类型。因此,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());
}

我们仍然允许使用 create of new post in draft state and ability to add text to the post content.但是,与其在 draft post 上使用返回空字符串的方法,不如让它这样做 草稿帖子根本没有该方法。这样,如果我们尝试获取 草稿帖子的内容,我们将收到一个编译器错误,告诉我们方法 不存在。因此,我们不可能意外地 在生产环境中显示草稿帖子内容,因为该代码甚至无法编译。 示例 17-19 显示了 struct 和 struct 的定义, 以及每个方法:Post::newcontentcontentPostDraftPost

文件名: 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

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

我们仍然有一个函数,但它不是返回 的实例,而是返回 的实例。因为是私有的 并且没有任何返回 的函数,因此无法创建 现在的实例。Post::newPostDraftPostcontentPostPost

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

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

那么我们如何获得已发布的帖子呢?我们希望强制执行 draft 帖子必须先经过审查和批准,然后才能发布。一个帖子 待审核状态仍不应显示任何内容。让我们实现 这些约束通过添加另一个结构 、 定义方法 on 以返回 、 和 定义一个方法 on 以返回 ,作为 如示例 17-20 所示:PendingReviewPostrequest_reviewDraftPostPendingReviewPostapprovePendingReviewPostPost

文件名: 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 转换为已发布 Postapprove 方法

的 and 方法具有 的所有权,因此 使用 和 实例 和 转换 它们分别被转换为 A 和 A 已发布。这边 在我们调用它们之后,我们不会有任何挥之不去的实例,依此类推。结构体不会 定义了一个方法,因此尝试读取其内容 会导致编译器错误,与 一样。因为获得 已发布的实例定义了一个 method,该实例的 API 是调用 的方法 ,而获取 A 的唯一方法是调用 上 的方法。 现在,我们已将博客文章工作流编码到类型系统中。request_reviewapproveselfDraftPostPendingReviewPostPendingReviewPostPostDraftPostrequest_reviewPendingReviewPostcontentDraftPostPostcontentapprovePendingReviewPostPendingReviewPostrequest_reviewDraftPost

但我们也必须对 .and 方法返回新实例,而不是修改它们所在的结构体 调用 on,因此我们需要添加更多的阴影分配来保存 返回的实例。我们也不能有关于 draft 和 待审核帖子的内容是空字符串,我们也不需要它们:我们不能 编译尝试不再使用这些状态下的帖子内容的代码。 更新后的代码如示例 17-21 所示:mainrequest_reviewapprovelet post =main

文件名: 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 的修改以使用新的 博客文章工作流的实施

我们需要对 reassign 进行的更改意味着 实现不再完全遵循面向对象的状态模式: 状态之间的转换不再完全封装 在 implementation 中。但是,我们的好处是无效状态是 现在不可能,因为类型系统和在 编译时间!这可确保某些 Bug(例如显示 未发布的帖子将在它们进入生产环境之前被发现。mainpostPost

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

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

总结

无论你是否认为 Rust 是一种面向对象的语言 阅读本章,您现在知道可以使用 trait objects 来获取一些 Rust 中的面向对象功能。动态 dispatch 可以为您的代码提供一些 灵活性,以换取一点运行时性能。您可以使用这个 灵活地实现面向对象的模式,这些模式可以帮助您的代码 可维护性。Rust 还有其他功能,比如所有权, 面向对象语言没有。面向对象的模式并不总是 是利用 Rust 优势的最佳方式,但 选择。

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准