实现状态模式
本节以面向对象设计模式中的状态模式为例,用 rust 来实现下面功能:
生成一份空白的草稿文档。
草稿完成后,请求对这篇草稿状态的文章进行审批。
在文章通过审批后正式发布。
仅返回成功发布后的文章,而不能发布没有通过审批的文章。
预期的效果
我们先看一下最终的使用方式:
// post/src/main.rs
use post::Post;
let mut post = Post::new();
post.add_text("中午我吃了沙拉");
println!("写稿中:{}", post.content()); // 写稿中:""
post.request_review();
println!("审批中:{}", post.content()); // 审批中:""
post.approve();
println!("已发布:{}", post.content()); // 已发布:"中午我吃了沙拉"
复制代码
首先定义文章结构体 Post:
// post/src/lib.rs
pub struct Post {
// 文章对应的状态对象,草稿、审批中、已发布分别对应不同的状态对象
// 至于为什么用Option类型,后面会解释
state: Option<Box<dyn State>>,
// 文章的内容
content: String,
}
复制代码
定义 State trait,相当于定义了一个状态的接口,要求每一种文章状态都实现状态接口中的方法:
trait State {
// 请求审批方法,注意签名中需要获取self所有权,让座返回一个新状态
// 由于当使用方法时,调用者是Box智能指针,例如:post.state.request_review()
// 所以request_review参数中需要对self进行签名标注Box<Self>
fn request_review(self: Box<Self>) -> Box<dyn State>;
// 发布方法,同样需要获取获取权,并返回新状态
fn approve(self: Box<Self>) -> Box<dyn State>;
// 获取文章的内容,不需要获取所有权,传入post对象,将其content返回
fn content<'a>(&self, post: &'a Post) -> &'a str;
}
复制代码
定义草案状态:
// 草案结构体
struct Draft {}
// 为Draft实现State状态
impl State for Draft {
// 实现请求审批方法,这里self获取了原状态的所有权,意味着原状态不可用了
fn request_review(self: Box<Self>) -> Box<dyn State> {
// 草案状态转换为审批中状态,这里返回审批中状态
Box::new(PendingReview {})
}
// 实现发布方法,同样获取原状态
fn approve(self: Box<Self>) -> Box<dyn State> {
// 由于草案状态是不能直接发布的,这里返回self意味着调用无效的意思
self
}
// 实现获取内容方法
fn content<'a>(&self, _post: &'a Post) -> &'a str {
// 草案状态下是不能获取内容的,所以这里返回空字符串
""
}
}
复制代码
定义审批中状态:
// 审批中结构体
struct PendingReview {}
// 为PendingReview实现State状态
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 {}
// 为Published实现State状态
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.as_str()
}
}
复制代码
实现 Post 转换状态的方法:
impl Post {
// 创建新的文章结构体
pub fn new() -> Self {
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 {
// 将状态从Option::Some中取出,由于不需要取得所有权,所以这里取出的是引用
if let Some(state) = &self.state {
// 将当前文章引用传递给状态对象
return state.content(&self)
} else {
""
}
// 更简单的做法:
// as_ref方法可以取出Some内容的引用,并返回Result类型,
// 接着使用unwrap取到state对象的引用
self.state.as_ref().unwrap().content(&self)
}
// 请求审批,需要改变状态,所以这里是可变引用
pub fn request_review(&mut self) {
// 由于Rust不允许存在空值,如果使用if let Some(state) = self.state
// 将会取到self.state的所有权,导致self.state为空了,
// 但是在state.request_review方法签名中我们是需要state的所有权的。
// 所以,这里使用Option<T>的take方法来取出state字段的Some值,
// 并在原来的位置留下一个None。这样做使我们能够将state的值从Post中移出来,
// 最终取到state的所有权。
if let Some(state) = self.state.take() {
// 转换为审批中状态
self.state = Some(state.request_review())
}
}
// 发布文章,同样需要改变状态
pub fn approve(&mut self) {
// 与请求发布状态转换一样,需要获取state的所有权
if let Some(state) = self.state.take() {
// 转换为发布状态
self.state = Some(state.approve())
}
}
}
复制代码
至此便利用状态模式实现的文章的发布流程。
优化实现
上面使用面向对象的状态模式,未来再添加一种状态时,只需要再实现一种状态即可,非常方便,但是代码中却有很多重复的地方,例如每种状态中的 request_review 和 approve 方法,还有 Post 结构体中的 request_review 和 approve 方法。
严格按照面向对象语言的定义来实现一套状态模式自然是可行的,但这并不能发挥出 Rust 的全部威力。下面会修改部分代码来使 post 库可以将无效的状态和状态转移暴露为编译时错误。
将状态转移实现为不同类型之间的转换:
// post/src/lib.rs
// 定义文章结构体
pub struct Post {
content: String
}
impl Post {
// 创建一篇文章的初始状态是草案类型
pub fn new() -> DraftPost {
DraftPost {
content: String::new()
}
}
// 获取文章的内容
pub fn content(&self) -> &str {
self.content.as_str()
}
}
// 定义草案结构体
pub struct DraftPost {
content: String
}
impl DraftPost {
// 只有草案支持添加文字
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
}
}
}
复制代码
再使用一下:
// post/src/main.rs
use post::Post;
// 获取草案
let mut draft = Post::new();
draft.add_text("hello,");
draft.add_text("world");
// 草案审批
let pending_review_post = draft.request_review();
// 草案审批通过,获得正式文章
let post = pending_review_post.approve();
println!("{}", post.content()); // hello,world
复制代码
经过上面的优化,剔除一些本不该属于某个状态下的一些方法,例如草案的发布方法、获取内容方法等,用户将不能再调用draft.content()
、draft.approve()
等一些无意义的方法,整体的代码更加精简。
封面图:跟着Tina画美国
关注「码生笔谈」公众号,阅读更多最新章节
评论