rust-unofficial / patterns

A catalogue of Rust design patterns, anti-patterns and idioms
https://rust-unofficial.github.io/patterns/
Mozilla Public License 2.0
8.02k stars 367 forks source link

State #227

Open simonsan opened 3 years ago

simonsan commented 3 years ago

Tracking issue for merging: https://github.com/lpxxn/rust-design-pattern/blob/master/behavioral/state.rs

Example:

//! State is a behavioral design pattern that lets an object alter its behavior when its internal state changes.
//! It appears as if the object changed its class.

//! We’ll implement a blog post workflow
//! 1. A blog post starts as an empty draft.
//! 2. When the draft is done, a review of the post is requested.
//! 3. When the post is approved, it gets published.
//! 4. Only published blog posts return content to print, so unapproved posts can’t accidentally be published.

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

struct Draft;
impl State for Draft {
    fn request_review(self: Box<Draft>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
    fn approve(self: Box<Draft>) -> Box<dyn State> {
        self
    }
}

struct PendingReview;
impl State for PendingReview {
    fn request_review(self: Box<PendingReview>) -> Box<dyn State> {
        self
    }
    fn approve(self: Box<PendingReview>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published;
impl State for Published {
    fn request_review(self: Box<Published>) -> Box<dyn State> {
        self
    }
    fn approve(self: Box<Published>) -> Box<dyn State> {
        self
    }
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
    fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
    fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
    fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

fn main() {
    let mut post = Post::new();

    let text = "State is a behavioral design pattern.";
    post.add_text(text);
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!(text, post.content());
    println!("post content: {}", post.content());
}
simonsan commented 3 years ago

Tracking discussion on merging: https://github.com/lpxxn/rust-design-pattern/issues/7

pickfire commented 3 years ago

I talked about this in https://github.com/rust-unofficial/patterns/pull/100#issuecomment-755043381