17 Rust的面向对象编程特性
面向对象编程(OOP)是一种模式话编程方式
17.3 面向对象设计模式的实现
状态模式是一个面向对象设计模式。它的关键在于一个值有很多内部状态,它们叫状态对象,同时每个状态对象都拥有自己的行为以及何时转变为另一种状态。值对状态对象的行为以及状态何时转移毫不知情
使用状态模式的好处在于业务需求发生变化时,我们不需要改变值或者操作值的代码。只需要转变状态对象的状态(包括增加状态对象)或变更每个状态对象的行为即可
我们来通过一个博客发布流程来说明上述功能
博客的功能:
1.博文从空白草案开始
2.一旦草案完成,请求审核博文
3.一旦博文过审,发表博文
4.打印发表的博文
我们将在blog库crate 中实现它
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("",post.content());
post.request_review();
assert_eq!("",post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
我们期望的实现如上,在这里我们可以发现post这个值他有几个内部状态,我们结下来会更加深刻的认识到这一点,测试函数仅仅是为了测试代码如期运行
现在让我们从基本的单位开始构建程序
定义Post并新建一个草案状态的实例
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(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
很好,我们创建了这个值的类型Post(调用的时候实例化就行),它是一个结构体,包含两个字段一个内容一个状态,状态还是可变的,在这里我们使用了一个Option枚举,里面装了一个trait对象 Box,没错,这里的操作和上一节中学过的内容时一致的
接着我们为Post定义了一个关联函数,函数返回的是一个Post实例,Post实例中content字段我们使用了String::from创建了一个空字符串,但是state字段我们使用了实现了State trait的一个一个结构体,很妙,一个状态对象就实现了
存放博文内容的文本
impl Post {
pub fn add_text(&mut self,text:&str) {
self.content.push_str(text);
}
}
为Post增加一个方法,用来为content中增加内容,这个非常简单直接定义就行了,它是和字段state无关的
确保博文草案的内容是空的
因为我们在现阶段需要保证博文是草案状态,所以即使调用add_text函数向博文中增加了内容之后,我们仍然需要让博文是一个草案,我们来再为Post增加一个字段同名方法content,让它总返回空字符串,这就实现我们的阶段需要
impl Post {
pub fn content(&self)->&str {
""
}
}
请求审核博文来改变其状态
前面我们已经实现了保持博文为草稿状态,并且能为其中增开内容。现在我们来让编辑完成的博文请求审核
前面博文的状态是由结构体Draft控制的,因此我们再来定义一个新结构体改变其状态
impl Post {
pub fn request_review(&mut self) {
if let Some(s) = self.state.take(){
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self:Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self:Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>)-> Box<dyn State> {
self
}
}
我们定义了一个结构体PendingReview,并且为它实现了 State trait,这样它就可以替代Draft了(因为它们都实现了 State trait)。那它到底怎么实现的呢?我们先为Post增加了方法request_review,它里面还有一个request_review方法,它会消费当前状态返回一个新状态。这个实现就这么完成了,我们进一步来看看
我们为 trait State增加了request_review方法(这里只定义了签名),这意味着所实现了 State trait的类型都需要实现request_review方法,具体方法体有一点点区别,前者是新建了一个PengingReview,后者是自己(也是PendingReview)
回到Post里的方法,我们使用了if let进行了匹配,并且把取出的老值作为参数给了request_review方法,它返回了一个新值PengingReview{}. Ok,这下真相大白了
增加改变content行为的approve方法
approve方法和request_review方法功能一样,我们为 State trait增加了approve方法,同样的,我们也为实现了State trait的类型统统增加了approve方法,并且它能切换状态
impl Post {
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self:Box<Self>) -> Box<dyn State>;
fn approve(self:Box<Self>) -> Box<dyn State>;
}
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
}
}
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{})
}
}
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
}
}
现在博文已经发布了,我们返回它的content
impl Post {
pub fn content(&self)->&str {
self.state.as_ref().unwrap().content(self)
}
}
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{
""
}
}
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_ref方法是因为需要Option中值的引用而不是获取其所有权,State是一个Option<Box>,调用as_ref会返回一个Option<&Box>
注意这个方法需要生命周期注解,这里获取post的引用作为参数,并返回post一部分的引用。所返回的引用的生命周期与post相关
好了,我么的例子讲完了,虽然代码量挺多,但是逻辑还算简单
状态模式的权衡与取舍
想象一下,如果我们不使用状态模式实现上述项目,我们可能会用一些match语句,会比较繁琐
但是使用状态模式,如果我们需要状态相互联系,如果在某些状态间增加状态,那就需要修改代码了。另一缺点是一些逻辑重复,还有一个缺点是调用时的重复如调用同一方法,后面我们常使用宏来修复这个问题
将状态和行为编码为类型
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("",post.content());
我们仍然可以新建一个post,但是这将使博文草案完全没有content方法
pub struct Post {
content:String,
}
pub struct DraftPost {
content:String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content:String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text &str) {
self.content.push_str(text);
}
}
通过类似上面的编译,我们能实现我们想要的功能,同时不想完全封装和状态转移,外部代码毫不知情
上述代码确保了博文从草案开始,并且草案博文没有任何展示内容
实现状态转移为不同类型的转换
我们来定义草案博文在发布之前必须被审核通过
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,
}
}
}
现在我们就把发布博文的工作流编码进了类型系统
因此,main函数也得修改
request_review和approve返回新实例而不是修改被调用的结构体,所我们要增加更多的let post = 覆盖赋值来保存返回的实例
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today",post.content());
}
我们的修改使得实现不再完全遵守面向对象的状态模式;状态间的转换不再完全封装在Post实现中。得益于类型系统和编译时类型检查,一些bug将会在部署到生产环境之前被发现
即便Rust能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在,这些模式有着不同的权衡取舍。所以要综合使用,某一种方法也许不一定是最好的方法
总结:Trait对象是一个Rust中获取部分面向对象功能的方法,动态分发可以通过牺牲少量运行时增加代码灵活性,这有助于实现代码可维护性的面向对象模式。Rust也有所有权这样不同于面向对象语言的功能
模式让Rust的功能变得多样和灵活,下一章我们将会一起学习
|