狀態模式(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
狀態轉換為一個新的狀態。
// 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
的聲明有關系,根路徑直接使用了包的名字。
// 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());
}