实现面向对象的设计模式
状态模式是一种面向对象的设计模式。该模式的关键在于,我们定义了一个值内部可以具有的一组状态。状态由一组状态对象表示,值的行为根据其状态而变化。我们将通过一个博客文章结构体的示例,该结构体有一个字段来保存其状态,该字段将是来自集合 “draft”、“review” 或 “published” 的状态对象。
状态对象共享功能:当然,在 Rust 中,我们使用结构和 trait 而不是对象和继承。每个 state 对象都负责自己的行为,并控制何时应更改为另一个 state。保存 state 对象的值对 state 的不同行为或何时在 state 之间转换一无所知。
使用状态模式的优点是,当程序的业务需求发生变化时,我们不需要更改保存状态的值的代码或使用该值的代码。我们只需要更新其中一个 state 对象中的代码来更改其规则或添加更多 state 对象。
首先,我们将以更传统的面向对象方式实现状态模式,然后我们将使用一种在 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!
以进行演示。一个很好的单元测试是断言 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 对象是 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
,然后将其添加为博客文章的文本内容。我们将其作为一种方法实现,而不是公开内容
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_text
的 Post
实例。然后我们调用
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
方法
Post
和 State
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 值的所有权。这是 Post
的 state
字段中的 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:在
Post
和 State
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
。
状态模式的一个缺点是,由于状态实现状态之间的转换,因此某些状态彼此耦合。如果我们在 PendingReview
和 Published
之间添加另一个状态,例如 Scheduled
,则必须更改 PendingReview
中的代码以转换为
Scheduled
改为。如果 PendingReview
不需要通过添加新状态来更改,那么工作量会更少,但这意味着切换到另一种设计模式。
另一个缺点是我们复制了一些逻辑。要消除一些
复制,我们可能会尝试为
request_review
并批准
State
trait 上返回 self
的方法;然而,这将违反 Object Safety,因为 trait 不知道具体的 self
到底是什么。我们希望能够将 State
用作 trait 对象,因此我们需要它的方法是对象安全的。
其他重复包括 request_review
的类似实现
和 approve
methods on Post
.两种方法都委托对 Option
的 state
字段中的值执行相同的方法,并将 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
Post
和 DraftPost
结构体都有一个私有内容
字段,用于存储博客文章文本。结构体不再具有 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_review
和 approve
方法获取 self
的所有权,从而使用 DraftPost
和 PendingReviewPost
实例并分别将它们转换为 PendingReviewPost
和已发布的 Post
。这样,在调用
request_review
他们,依此类推。PendingReviewPost
结构没有定义 content
方法,因此尝试读取其内容会导致编译器错误,就像 DraftPost
一样。因为获取定义了 content
方法的已发布 Post
实例的唯一方法是在 PendingReviewPost
上调用 approve
方法,而获取
PendingReviewPost
是在 DraftPost
上调用 request_review
方法,我们现在已将博客文章工作流编码到类型系统中。
但是我们也必须对 main
进行一些小的更改。request_review
和
approve
方法返回新实例,而不是修改调用它们的 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 的另一个功能,可以带来很大的灵活性。我们在整本书中简要地研究了它们,但还没有看到它们的全部功能。我们走吧!