实现一种面向对象设计模式
Implementing an Object-Oriented Design Pattern
状态模式,the state pattern 属于一种面向对象设计模式。这种模式的核心,便是咱们要定义某个值在其内部可能有的一套各种状态。这些状态是由一套 状态对象,state objects 所表示的,同时该值的行为,会根据其状态而改变。接下来咱们会完成有着保存其状态字段,该字段将有着“草稿,draft”、“审阅,review” 或“已发布,published” 三种状态集合的状态对象,的一个博客帖子结构体示例。
状态对象共用着功能:当然,在 Rust 中咱们会使用结构体与特质,而非对象与继承。每个状态对象负责其自己的行为,以及在其应变换为另一状态时自身的治理。保存着状态对象的值,对这些状态的不同行为,或这些状态之间何时变换就毫不知情。
运用状态模式的优势在于,当程序的业务需求改变时,咱们将不需要修改该值保存状态的那些代码,也不需要修改用到该值的那些代码。咱们只需更新某个状态对象内部的那些代码,来改变其规则,或是添加别的一些状态对象。
首先,咱们将要以更传统的面向对象方式,实现这种状态模式,随后咱们将使用对于 Rust 中,更自然一些的方法。下面就来深入到使用状态模式,逐步实现一个博客帖子工作流。
最终功能看起来将像下面这样:
-
博客帖子以一个空的草稿开始;
-
在草稿写好后,该帖子就要求审阅一下;
-
在帖子被批准后,其就会被发布;
-
只有发布了的帖子,才会返回要打印的内容,因此那些未获批准的帖子就不会被无故发布。
所有别的在帖子上的尝试修改,都应无效。比如,在完成审阅之前,若咱们尝试批准博客帖子草稿,那么该帖子应保持为一个未发布的草稿。
下面清单 17-11 给出了代码形式的这个工作流:此为咱们将在一个名为 simple_blog
的库代码箱中,实现的这个 API 的一个示例用法。由于咱们尚未实现该 simple_blog
代码箱,因此这段代码尚不会编译。
文件名:src/main.rs
use simple_blog::Post; fn main() { let mut post = Post::new(); post.add_text("今天午饭我吃了沙拉。"); assert_eq! ("", post.content()); post.request_review(); assert_eq! ("", post.content()); post.approve(); assert_eq! ("今天午饭我吃了沙拉。", post.content()); }
清单 17-11:验证咱们打算这个 simple_blog
代码箱要有的必要功能的代码
咱们打算运行用户使用 Post::new
创建出一个新的帖子草稿。咱们打算允许将一些文本添加到博客帖子。当咱们在审批之前,尝试立即获取到帖子的内容时,由于该帖子仍为一个草稿,因此咱们就不应得到任何文本。出于验证目的,咱们已在该代码中添加了 assert_eq!
。而为此目的的良好单元测试,就应断言帖子草稿会从那个 content
方法返回空字符串,而咱们并未打算为此示例编写一些测试。
接下来,咱们打算开启该帖子的审阅请求,同时咱们系统在等待审阅期间,content
返回一个空字符串。在该帖子得到审批时,他就应得以发布了,表示在 content
被调用时,该帖子的文本将被返回。
请注意咱们与 simple_blog
代码箱交互的唯一类型,便是 Post
这个类型。此类型将用到状态模式,并将保存着将为表示帖子可能状态 -- 草稿、等待审阅或已发布,的三个状态对象之一的一个值。从一种状态改变为另一状态,将在该 Post
类型里得以内部管理。这些状态,会因应着库用户在 Post
实例上的方法调用而改变,但库用户们却不必直接管理这些状态改变。同样,用户们是无法在这些状态上犯下错误的,比如在帖子未审阅前发布帖子。
定义出 Post
并创建出一个草稿状态的新实例
Defining Post
and Creating a New Instance in the Draft State
下面就来开始这个库的实现!咱们清楚咱们需要保存着一些内容的一个公开的 Post
结构体,因此咱们将以这个结构体的定义,及创建出 Post
实例的一个关联的公开 new
函数开始,如下清单 17-12 中所示。咱们还将构造出将定义 Post
的全部状态对象,所必须有的行为的一个私有 State
特质。
随后 Post
类型将在名为 state
的私有字段的 Option<T>
值内部,保存 Box<dyn State>
类型的一个特质对象,来保存状态对象。过一会儿,咱们就会看到为何那个 Option<T>
是必要的。
文件名:src/lib.rs
#![allow(unused)] fn main() { 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
实例的 new
函数、State
特质,以及 Draft
结构体
其中的 State
特质会定义出由不同帖子状态共用的行为。这些状态对象分别是 Draft
、PendingReview
及 Published
,同时他们都将实现 State
特质。至于现在,这个特质并无任何方法,而由于 Draft
状态是咱们想要帖子开始的状态,因此咱们将以仅定义出这个状态开始。
在创建出新的 Post
实例时,咱们将其 state
自动设置为了保存着一个 Box
值的 Some
值。这个 Box
会只想 Draft
结构体的一个新实例。这会确保不能在咱们何时创建出一个 Post
的新实例,其都将作为一篇草稿开始。由于 Post
的 state
字段是私有的,因此就没有办法创建出其他任何状态的一个 Post
!在 Post::new
函数中,咱们把 content
字段设置为了一个新的、空 String
。
存储帖子内容文本
Storing the Text of the Post Content
在 17-11 中,咱们曾看到咱们希望能调用一个名为 add_text
的方法,并传递给他随后被作为博客帖子内容而添加的一个 &str
。咱们将这实现为一个方法,而不是把 content
作为 pub
暴露出来,如此稍后咱们就可以实现一个将控制 content
字段数据如何被读取的方法。这个 add_text
方法是相当直截了当的,那么接下就来在清单 17-13 中,添加这个实现到 impl Post
代码块:
文件名:src/lib.rs
#![allow(unused)] fn main() { impl Post { // -- 跳过代码 -- pub fn add_text(&mut self, text: &str) { self.content.push_str(text); } } }
清单 17-13:实现将文本添加到帖子 content
字段的 add_text
方法
由于咱们是在修改咱们正于其上调用 add_text
的 Post
实例,因此这个 add_text
方法便取了到 self
的可变引用。咱们随后调用了 content
字段中 String
类型上的 push_str
,并传递 text
参数来添加到那个已保存的 content
。此行为不依赖帖子所处的状态,因此其并非状态模式的一部分。这个 add_text
方法完全不与 state
字段交互,但其为咱们打算支持行为的一部分。
确保帖子草稿的内容为空
Ensuring the Content of a Draft Post Is Empty
即使咱们已调用了 add_text
并把一些内容添加到了咱们的帖子,但由于该帖子仍处于草稿状态,故咱们仍想要那个 content
方法返回空字符串切片,an empty string slice,正如清单 17-11 中第 7 行所给出的那样。那么现在,就来用将满足此要求的最简单物件,实现这个 content
方法:即总是返回一个空字符串切片。稍后一旦咱们实现修改帖子状态的能力,从而帖子可被发布,咱们就会修改这个方法。到目前为止,贴子就只能处于草稿状态,因此帖子内容应始终为空。下面清单 17-14 给出了这种占位的实现:
文件名:src/lib.rs
#![allow(unused)] fn main() { impl Post { // -- 跳过代码 -- pub fn content(&self) -> &str { "" } } }
清单 17-14:添加始终返回空字符串切片的 Post
上 content
方法的一个占位实现,a placeholder implementation
有了添加的这个 content
方法,清单 17-11 中到第 7 行为止的那些代码就都将如预期那样工作了。
请求帖子审阅改变其状态
Requesting a Review of the Post Changes Its State
接下来,咱们就需要添加请求帖子审阅的功能了,帖子审阅应将其状态从 Draft
改变为 PendingReview
。下面清单 17-15 给出了这样的代码:
文件名:src/lib.rs
#![allow(unused)] fn main() { impl Post { // -- 跳过代码 -- 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:实现 Post
与 State
特质上的 request_review
方法
咱们给了 Post
名为 request_review
的一个公开方法,其将取到 self
的一个可变引用。随后咱们调用了 Post
当前状态上的内部 request_review
方法,而这第二个 request_review
方法就会消费当前状态并返回一个新的状态。
咱们把那个 request_review
方法添加到了 State
特质;所有实现了这个特质的类型,现在都将需要实现这个 request_review
方法。请注意这里用的不再是 self
、&self
或 &mut self
作为该方法的首个参数,这里用的是 self: Box<Self>
。这样的语法表示,在有在某个保存了该类型的 Box
上调用时,这个方法才是有效的。这种语法会取得 Box<Self>
的所有权,令到原有状态失效,进而 Post
的状态值就可以转换到一种新的状态。
为了消费原有状态,这个 request_review
方法就需要取得该状态值的所有权。这便是 Post
的那个 state
字段中的 Option
进入之处:咱们调用了 take
方法(属于标准库的 Option
类型),来从 state
字段取出那个 Some
的值,并由于 Rust 不允许咱们在结构体中有无效或空字段,Rust doesn't let us have unpopulated fields in structs,而在 state
字段中留下一个 None
。这样就允许咱们把其中的 state
值,迁移出 Post
而非借用他。随后咱们将把帖子的 state
值,设置为此操作的结果。
为了获取到 state
值的所有权,咱们就需要暂时将 state
设置为 None
,而非直接使用像是 self.state = self.state.request_review();
这样的代码设置他。这样做确保了在咱们已将 Post
转换为新状态后,其无法使用原先的 state
值。
Draft
上的 request_review
方法返回的是个新的、新加入的 PendingReview
装箱过的实例,其表示了帖子等待审阅时的状态。那个 PendingReview
结构体同样实现了 request_review
方法,但并未进行任何转换。而是,由于在咱们于某个已处于 PendingReview
状态的帖子上,请求审阅时,帖子应保持处于 PendingReview
状态,因此 PendingReview
的 request_review
方法调用返回的是他自己。
现在咱们就可以开始看到状态模式的优势了:不论 Post
的 state
值为何,其上的 request_review
方法都是一样的。每种状态都负责着其自己的那些规则。
咱们将保留 Post
上的 content
方法如其现在这样,即返回一个空字符串切片。现在咱们就可以让某个 Post
,处于 PendingReview
状态抑或 Draft
状态了,不过咱们想要 PendingReview
状态中的同样行为。现在清单 17-11 到第 10 行便工作了!
添加 approve
来修改 content
的行为
Adding approve
to Change the Behavior of content
approve
方法将与 request_review
方法类似:他将把 state
设置为在帖子状态为 “批准” 时,当前状态所应表明的值,如下清单 17-16 中所示:
文件名:src/lib.rs
#![allow(unused)] fn main() { impl Post { // -- 跳过代码 -- 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 approve(self: Box<Self>) -> Box<dyn State> { self } } struct PendingReview {} impl State for PendingReview { // -- 跳过代码 -- 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
特质上的 approve
方法
咱们把这个 approve
方法,添加到了 State
特质,并添加了一个实现了 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
方法,如下清单 17-17 中所示:
文件名:src/lib.rs
#![allow(unused)] fn main() { impl Post { // -- 跳过代码 -- pub fn content(&self) -> &str { self.state.as_ref().unwrap().content(self) } // -- 跳过代码 -- } }
清单 17-17:将 Post
上的 content
方法,更新为委托给 State
上的 content
方法
由于咱们的目标是要把所有规则,都保持于实现了 State
的那些结构体中,因此咱们就要调用 state
字段中值上的 content
方法,并将帖子实例(那就是 self
)作为参数加以传递。随后咱们要返回从 state
值上的 content
方法调用所返回的值。
由于咱们要的是到 Option<T>
内部值的一个引用,而非该值的所有权,因此咱们调用了 Option<T>
上的 as_ref
方法。由于 state
是个 Option<Box<dyn State>>
,在咱们调用 as_ref
时,就会返回一个 Option<&Box<dyn State>>
。而若咱们没有调用 as_ref
,那么由于咱们无法无法把 state
迁移出那个借用的函数参数 &self
,而将得到一个报错。
咱们随后调用了 unwrap
方法(标准库 Option<T>
类型上的),由于咱们清楚,Post
上的那些方法,会确保 state
将在这些方法完成时,始终包含某个 Some
值,因此咱们就明白,这个unwrap
是绝不会终止运行的。这便是第 9 章 相比与编译器咱们掌握着更多信息的情形 小节所讲到的情形之一:即咱们明白某个 Option<T>
不可能是个 None
值,尽管编译器无法掌握这一点。
到 unwrap
方法这里,当咱们在 &Box<dyn State>
上调用 content
方法时,强制解引用转换,deref coercion 就会在那个 &
及 Box
上发挥作用,从而 content
方法就将在实现了 State
特质的类型上,最终被调用到。而那就意味着咱们需要把 content
添加到 State
特质的定义,而那正是咱们把根据咱们所有的状态,返回什么样的内容,这种逻辑要放入的地方,如下清单 17-18 中所示:
文件名:src/lib.rs
#![allow(unused)] fn main() { trait State { // -- 跳过代码 -- fn content<'a>(&self, post: &'a Post) -> &'a str { "" } } // -- 跳过代码 -- struct Published {} impl State for Published { // -- 跳过代码 -- fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content } } }
清单 17-18:把 content
方法添加到 State
特质
咱们添加了返回空字符串切片的 content
方法默认实现。那就意味着咱们无需在 Draft
与 PendingRereview
两个结构体上实现 content
方法。而 Published
结构体则将重写这个 content
方法,并返回 post.content
中的值。
注:由于
content
默认实现返回的是""
空字符串切片,是个已知大小的值,故方才可以写默认实现。而若将request_review
或approve
也写为默认实现,即如下面这样:
#![allow(unused)] fn main() { trait State { 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 { "" } } }
那么将报出错误:
$ cargo run ✔
Compiling simple_blog v0.1.0 (/home/peng/rust-lang/simple_blog)
error[E0277]: the size for values of type `Self` cannot be known at compilation time
--> src/lib.rs:40:53
|
40 | fn approve(self: Box<Self>) -> Box<dyn State> { self }
| ^^^^ doesn't have a size known at compile-time
|
= note: required for the cast from `Self` to the object type `dyn State`
help: consider further restricting `Self`
|
40 | fn approve(self: Box<Self>) -> Box<dyn State> where Self: Sized { self }
| +++++++++++++++++
For more information about this error, try `rustc --explain E0277`.
error: could not compile `simple_blog` due to previous error
这表示 Rust 中的默认实现,需要返回值为固定大小。
请注意正如咱们曾在第 10 章中讨论过的那样,在这个方法上咱们需要生命周期注解。咱们取了到某个 post
的引用作为参数,并返回的是到那个 post
一部分的引用,因此所返回引用的生命周期,便于这个 post
参数生命周期相关。
而咱们就完成了 -- 清单 17-11 的全部代码现在便工作了!咱们就已实现了博客帖子工作流规则,the rules of the blog post workflow 下的状态模式。与那些规则相关的逻辑,是存在与这些状态对象中,而非散落于 Post
的各处,the logic related to the rules lives in the state objects rather than being scattered throughout Post
。
为何不用枚举,Why Not An Enum?
你可能已经想到,为何咱们没有使用将不同帖子状态作为变种的一个
enum
。那确实是一种可行的办法,请尝试并比较最后的结果,来发现你要选哪个方案!运用枚举的一个不足之处,便是在每个检查枚举值的地方,将都需有一个match
表达式,或类似的东西来处理每种可能的变种。相比这里的特质对象方法,那就会有更多重复代码。
状态模式的取舍
Trade-offs of the State Pattern
咱们已经证明,Rust 是能够实现这种面向对象模式,来封装处于不同状态下帖子应具备的各种不同行为。Post
上的方法对这些各种行为毫不知情。咱们组织代码的方式,即咱们必须仅在一处查看,而获悉某个已发布帖子可以有的那些不同行为方式:这便是 Published
结构体上 State
特质的实现。
若咱们原本打算创建另一种不使用状态模式的替代实现,那么咱们可能就会在 Post
上的那些方法中,使用一些检查帖子状态的 match
表达式,并在那些 match
表达式处改变行为。那就意味着咱们将不得不查看多个地方,来了解某个处于已发布状态帖子的全部影响!这样做只会徒增咱们所添加的更多一些状态:每个的这些 match
表达式,都将需要另一支臂。
而在状态模式下,那些 Post
方法以及那些咱们用到 Post
的各处,就不需要那些 match
表达式,而要添加一个新状态,咱们将只需添加一个新结构体,并在那个结构体上实现那些特质方法即可。
使用状态模式的这种实现,易于添加更多功能。为发现使用状态模式维护代码的简单性,请尝试下面几条建议:
-
请添加将帖子状态从
PendingReview
改回到Draft
的一个reject
方法; -
在状态可被改变为
Published
之前,要求两次到approve
的调用; -
只有在某个帖子处于
Draft
状态时,才允许用户添加文本内容。提示:让状态对象负责那些可能修改内容的操作,而不负责修改Post
的操作。
状态模式的一个缺点则是,由于这些状态都实现那些状态间的转换,那么其中一些状态就相互耦合了。当咱们在 PendingReview
于 Published
之间,添加另一状态,比如 Scheduled
时,咱们将不得不把 PendingReview
中的代码,修改为相应地转换到 Scheduled
。若在新状态的添加下,PendingReview
无需修改,那么就会少一些事情,然而那便意味着转换到另一种涉及模式了。
至于另一个缺点,便是咱们重复了一些逻辑。为消除一些重复,咱们就可能会尝试构造 State
特质上,返回 self
的 request_review
于 approve
两个方法的默认实现;然而,由于该特质不清楚那个具体的 self
将为何物,因此这会违反对象安全性,violate object safety。咱们希望能够将 State
作为特质对象使用,因此咱们就需要他的那些方法是对象安全的。
其他代码重复包括了 Post
上 request_review
与 approve
两个方法的一些相似实现。这两个方法都委托给了那个 Option
的 state
字段中值上的同一方法,并将 state
字段的值,设置到方法的结果。若咱们在 Post
上有着大量的遵循这种模式的方法,咱们就会考虑定义出一个宏,defining a macro,来消除这种重复(请参阅第 19 章中 "宏,Macros" 小节)。
经由这种完全按照面向对象模式下所定义的状态模式,来实现这种模式,咱们就没有利用上原本所能利用的 Rust 的全部优势。下面就来看看,咱们可对那个 simple_blog
能做出的,可将无效状态与无效状态转换,构造为编译时错误的一些改变。
将状态与行为编码为类型
Encoding States and Behavior as Types
咱们将给出如何对这种状态模式加以反思,以得到一套不同的权衡取舍。不同于对状态及状态的转换进行完全地封装,进而外部代码对他们一无所知,咱们将把那些状态编码为不同类型。于是乎,Rust 的类型检查系统,就将通过发出编译器报错,阻止在那些仅允许已发布帖子之处,使用草稿帖子的尝试。
下面来考虑一下清单 17-11 中,main
函数的第一部分:
文件名:src/main.rs
fn main() { let mut post = Post::new(); post.add_text("今天午饭我吃了沙拉。"); assert_eq! ("", post.content()); }
咱们仍旧使用 Post::new
,实现了新的处于草稿状态的那些帖子的创建,并实现了将文本添加到帖子内容的能力。但与在草稿帖子上有着返回空字符串的 content
方法不同,咱们将把 Post
构造为根本就没有那个 content
方法。那样的话,在咱们尝试获取某个草稿帖子的内容时,就会得到告诉咱们该方法不存在的编译器报错。由此,对于生产中咱们无意地显示出帖子内容,由于那样的代码甚至都不会编译,那么这将是不可能的了。清单 17-19 给出了 Post
结构体的定义,以及一个 DraftPost
的结构体,以及各自上的一些方法:
文件名:src/lib.rs
#![allow(unused)] fn main() { 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
Post
与 DraftPost
两个结构体都有着存储了博客帖子文本的私有 content
字段。由于咱们正将状态编码,迁移到一些结构体类型,因此这两个结构体就不再有 state
字段了。Post
结构体将表示已发布帖子,而他便有着返回 content
的 content
方法。
咱们仍有一个 Post::new
函数,但不是返回 Post
实例,其返回的是 DraftPost
实例。由于 content
是私有的,而有没有任何返回 Post
的函数,那么此刻就不可能创建出 Post
实例。
DraftPost
结构体有着一个 add_text
方法,因此咱们就可以如同之前那样,把文本添加到 content
,但请注意 DraftPost
并没有定义一个 content
方法!因此现在的程序确保了全部帖子都以草稿帖子开头,而草稿帖子并不会让他们的内容用于显示。任何绕过这些约束的尝试,都将导致编译器报错。
以到不同类型的转换,实现(状态的)转换
Implementing Transitions as Transformations into Different Types
那么怎样来获取到某个已发布帖子呢?咱们是打算强化某个草稿帖子在其可被发布之前,必须被审阅和批准的规则。处于等待审阅状态的帖子,应仍然不显示任何内容。下面酒类通过添加另一结构体,PendingReviewPost
、在 DraftPost
上定义出返回 PendingReviewPost
实例的 request_review
方法,以及在 PendingReviewPost
上定义出返回 Post
的 approve
方法,实现这些约束,如下清单 17-20 中所示:
文件名:src/lib.rs
#![allow(unused)] fn main() { impl DraftPost { // -- 跳过代码 -- 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
转变为已发布 Post
的 approve
方法
request_review
与 approve
两个方法,都取得了 self
的所有权,从而消费了 DraftPost
及 PendingReviewPost
实例,并把他们相应地转换成了 PendingReviewPost
与已发布的 Post
。以这种方式,在咱们于 DraftPost
实例上调用了 request_review
之后,便不会再有任何遗存的 DraftPost
实例,对 PendingReviewPost
之类亦是如此。PendingReviewPost
结构体之上,并没有 content
方法,因此正如 DraftPost
一样,尝试读取其内容,会导致编译器报错。由于获取确实有定义出的 content
方法的已发布 Post
实例的唯一方式,为在某个 PendingReviewPost
上调用 approve
方法,而获取到一个 PendingReviewPost
的唯一方法,为在某个 DraftPost
上调用 request_review
方法,咱们现在便已将这个博客帖子工作流,编码为了类型系统。
不过咱们还必须对 main
做出一些小的修改。request_review
与 approve
两个方法,返回的都是一些新实例,而不再是修改他们于其上所调用的结构,因此咱们就需要添加更多 let post =
遮蔽赋值语句,来保存那些返回的实例。咱们还不能断言草稿于等待审阅帖子的内容为空字符串,咱们也是不需要他们的:咱们再也不会编译,尝试使用处于这些状态下的帖子内容的代码。下面清单 17-21 中,给出了 main
中更新后的代码:
文件名:src/main.rs
use neo_simple_blog::Post; fn main() { let mut post = Post::new(); post.add_text("这是一个博客帖子。"); let post = post.request_review(); let post = post.approve(); assert_eq! ("这是一个博客帖子。", post.content()); }
清单 17-21:为使用这个博客帖子工作流的新实现,而对 main
的一些修改
咱们所需做出的对 post
重新复制的那些修改,表示这种实现,已不再那么严格遵循面向对象设计模式了:状态之间的转换,不再是整个地封装在 Post
实现里。不过,咱们的收获,则是由于类型系统,以及编译时所发生的类型检查,那些无效状态现在就不可能了!这样就确保了一些确切代码错误,比如未发布帖子内容的显示等,在其到达生产部署之前,就将被发现。
请在清单 17-21 之后的情况下,尝试在 neo_simple_blog
上实现本小节开头给出的那些任务,来发现你对此版本代码的设计模式有何看法。请注意其中一些任务,在这种模式下或许已被完成了。
咱们业已看到,即使 Rust 有能力实现面向对象的一些设计模式,而对于其他模式,比如将状态编码为类型系统等,在 Rust 中也都是可行的。这些模式都有着不同的取舍。尽管咱们可能对面向对象的那些模式非常熟悉,但对问题进行反思,而运用上 Rust 那些特性的优势,就可以提供到各种好处,比如在编译时阻止一些代码错误等。出于比如所有权这样的,面向对象语言所不具备的某些特性,那么在 Rust 中,面向对象的那些模式,将并不总是最佳方案。
本章小节
在读完这一章之后,不论咱们认为或是不认为 Rust 是门面向对象语言,现在都明白,咱们可以在 Rust 中,使用特质对象来获得一些面向对象的特性。动态调遣,dynamic dispatch 可以些许运行时性能损失,换取到咱们代码一定程度的灵活性。咱们则可运用这样的灵活性,来实现能有助于代码可维护性的一些面向对象模式。Rust 还有面向对象语言所没有的其他一些特性,比如所有权等。对于利用 Rust 各种长处方面的优势来讲,面向对象模式将不总是最佳方式,但其为一种可行选项。
接下来,咱们将看看各种模式,这是带来大量灵活性的 Rust 诸多特性的另一项。虽然贯穿本书,咱们已经粗略地看了看他们,但仍尚未见识到他们的完整能力。咱们就拭目以待吧!