状态模式(state pattern
)是一种面向对象的设计,它的关键点在于:一个值拥有的内部状态由数个状态对象(state object
)表的而成,而值的行为则随着内部状态的改变而改变。
下面的示例用来实现发布博客的工作流程:
- 在新建博客时生成一个空白的草稿文档,状态是
Draft
。 - 在草稿编辑完毕后,请求对这个文章的状态进行审批(
request_review
),文档此时状态切换成了PendingReview
。 - 文章通过审批后对外正式发布,状态为
Published
。 - 仅返回并打印成功发布后的文章,其他状态的文章都应该是对外不可见的
State trait
定义了文章状态共享的行为,状态Draft
、PendReview
、Published
都会实现State trait
。
State trait
中request_review
声明中,第一个参数的类型是self: Box<Self>
,而不是self
、&self
或&mut self
。这个语法意味着该方法只能被包裹着当前实例的Box
调用,它会在调用过程中获取Box<Self>
的所有权并使旧的状态失效,从而将Post
状态转换为一个新的状态。
rust
// lib.rs
trait State {
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;
}
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())
}
}
}
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
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
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 {})
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
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
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
request_review
为了消耗旧的状态,request_review
方法需要获取状态值的所有权。这也正是Post
的state
字段引入Option
的原因:RUST
不允许结构体中出现未被填充的值。我们可以通过Option<T>
的take
方法来取出state
字段的Some
值,并在原来的位置留下一个None
。
我们需要临时把state
设置为None
来取得state
值的所有权,而不能直接使用self.state = self.state.request_review()
这种代码,这可以确保Post
无法在我们完成状态转换后再次使用旧的state
值。
take
方法的作用:Takes the value out of the option, leaving a [
None] in its place.
content
content
方法体中调用了Option
的as_ref
方法,因为我们需要的只是Option
中值的引用,而不是它的所有权。由于state
的类型是Option<Box<dyn State>>
,所以我们在调用as_ref
时得到Option<&Box<dyn State>>
。如果这段代码中没有调用as_ref
,就会导致编译错误,因为我们不能将state
从函数参数的借用&self
中移出。
我们需要在这个方法上添加相关的声明周期标注,该方法接受post
的引用作为参数,并返回post
中的content
作为结果,因此,该方法中返回值的声明周期应该与post
参数的声明周期相关。
as_ref
方法声明:Converts from&Option<T>
toOption<&T>
,但例子中属于Option<T>
到Option<&T>
的转换
代码冗余
示例的代码存在一个缺点:Draft
、PendReview
、Published
重复实现了一些代码逻辑。你也许会试着提供默认实现,让State trait
的request_review
和approve
方法默认返回self
。但这样的代码违背了对象安全规则,因为trait
无法确定self
的具体类型究竟是什么。如果我们希望将State
作为trait
对象来使用,那么它的方法就必须全部是对象安全的。
use
代码中使用了main.rs
和lib.rs
两个文件,在lib.rs
也没有做任何mod
的声明。在main.rs
中通过使用blog::Post
路径进行导,而不是crate::Post
。
根路径blog
和我们配置文件Cargo.toml
中package.name
的声明有关系,根路径直接使用了包的名字。
rust
// main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("l go out to play");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("l go out to play", post.content());
}