5分钟速读之Rust权威指南(三十五)状态模式

实现状态模式

本节以面向对象设计模式中的状态模式为例,用rust来实现下面功能:

  1. 生成一份空白的草稿文档。
  2. 草稿完成后,请求对这篇草稿状态的文章进行审批。
  3. 在文章通过审批后正式发布。
  4. 仅返回成功发布后的文章,而不能发布没有通过审批的文章。

预期的效果

我们先看一下最终的使用方式:

// 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画美国

关注「码生笔谈」公众号,阅读更多最新章节

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享