跳转至

12. 面向对象篇

对应章节:第17章(trait 对象)。
本篇掌握 Rust 的面向对象风格:trait 对象与状态模式。


项目 19:博客文章状态机

目标:用 trait 对象实现"状态模式",理解 self: Box<Self>。

创建项目

cargo new blog --lib
cd blog

说明:这是库项目(--lib),核心状态机放 src/lib.rs,测试也在其中。如需命令行演示,可自行添加 src/main.rs(use blog::Post;)。

功能要求

状态流转:草稿 → 审核中 → 已发布

let mut post = Post::new();
post.add_text("今天学了 Rust");
assert_eq!(post.content(), "");          // 草稿不显示
post.request_review();
assert_eq!(post.content(), "");          // 审核中不显示
post.approve();
assert_eq!(post.content(), "今天学了 Rust");   // 已发布才显示

知识点

知识点 说明
Box<dyn State> trait 对象作为状态字段
self: Box<Self> 按值接收装箱自身,可消耗并返回新状态
Option::take() 拿走旧状态换新状态
默认方法 trait 方法默认实现
状态模式 每个状态封装自己的转换规则

代码

src/lib.rs:

pub struct Post {
    // state 用 Box<dyn State> 存"当前状态"(trait 对象,运行期才知道具体状态)
    // Option 包装:转换状态时 take() 拿走旧状态,允许临时为空
    state: Option<Box<dyn State>>,
    content: String,
}

// 三个具体状态:空结构体,只承载"身份"
struct Draft;
struct PendingReview;
struct Published;

// 状态 trait:定义所有状态共有的行为
trait State {
    // self: Box<Self>:按值接收"装箱的自身",消耗旧状态并返回新状态
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
    // 默认实现:默认不显示内容,只有 Published 覆盖它
    fn content<'a>(&self, _post: &'a Post) -> &'a str {
        ""
    }
}

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   // 草稿不能直接通过,保持原样
    }
}

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)   // 审核通过 → 已发布
    }
}

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
    }
}

impl Post {
    pub fn new() -> Self {
        Self {
            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) {
        // take() 拿走旧状态 → 调用其方法得到新状态
        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());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn draft_not_shown() {
        let mut post = Post::new();
        post.add_text("hello");
        assert_eq!(post.content(), "");
    }

    #[test]
    fn full_workflow() {
        let mut post = Post::new();
        post.add_text("hello");
        post.request_review();
        assert_eq!(post.content(), "");
        post.approve();
        assert_eq!(post.content(), "hello");
    }
}

src/main.rs:

use blog::Post;

fn main() {
    let mut post = Post::new();
    post.add_text("今天学了 Rust的状态模式");

    println!("草稿阶段,内容:{}", post.content());

    post.request_review();
    println!("审核中,内容:{}", post.content());

    post.approve();
    println!("已批准,内容:{}", post.content());
}

运行

cargo test          # 2 passed
cargo run
# 草稿阶段,内容:
# 审核中,内容:
# 已批准,内容:今天学了 Rust的状态模式

注意事项(重点)

  1. self: Box<Self> 是状态转换的关键:方法按值吃掉 Box 里的自身,用完即弃并返回新状态——把"旧状态消失、新状态诞生"表达为天然语义。若用 &self 只能借用,无法消耗旧状态。
  2. Option 的必要性:self.state.take() 拿走旧 Box 瞬间字段为空,Option 允许这种"临时为空"。
  3. content 的默认方法:Draft/PendingReview 继承返回 "",Published 覆盖返回 &post.content;生命周期 'a 绑定 post 的借用。
  4. trait State 私有也没关系:Post 的公开方法通过内部调用 trait 方法,外部无需知道状态类型——封装了状态机。
  5. 改进练习:目前没有覆盖"草稿直接 approve 无效"这条路径的测试——自己写一个:新建 Post 后直接调用 approve(),断言内容仍为空。

评论