实现状态模式
本节以面向对象设计模式中的状态模式为例,用 rust 来实现下面功能:
生成一份空白的草稿文档。
草稿完成后,请求对这篇草稿状态的文章进行审批。
在文章通过审批后正式发布。
仅返回成功发布后的文章,而不能发布没有通过审批的文章。
预期的效果
我们先看一下最终的使用方式:
// post/src/main.rsuse 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.rspub 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.rsuse 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画美国
关注「码生笔谈」公众号,阅读更多最新章节
评论