Adding approve to Change the Behavior of content
The approve method will be similar to the request_review method: it will set state to the value that the current state says it should have when that state is approved, as shown in Listing 17-16.
Filename: src/lib.rs
impl Post {
--snip--
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 {
--snip--
fn approve(self: Box<Self>) -> Box<dyn State> {
1 self
}
}
struct PendingReview {}
impl State for PendingReview {
--snip--
fn approve(self: Box<Self>) -> Box<dyn State> {
2 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
}
}
Listing 17-16: Implementing the approve method on Post and the State trait
We add the approve method to the State trait and add a new struct that implements State, the Published state.
Similar to the way request_review on PendingReview works, if we call the approve method on a Draft, it will have no effect because approve will return self [1]. When we call approve on PendingReview, it returns a new, boxed instance of the Published struct [2]. The Published struct implements the State trait, and for both the request_review method and the approve method, it returns itself because the post should stay in the Published state in those cases.
Now we need to update the content method on Post. We want the value returned from content to depend on the current state of the Post, so we're going to have the Post delegate to a content method defined on its state, as shown in Listing 17-17.
Filename: src/lib.rs
impl Post {
--snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
--snip--
}
Listing 17-17: Updating the content method on Post to delegate to a content method on State
Because the goal is to keep all of these rules inside the structs that implement State, we call a content method on the value in state and pass the post instance (that is, self) as an argument. Then we return the value that's returned from using the content method on the state value.
We call the as_ref method on the Option because we want a reference to the value inside the Option rather than ownership of the value. Because state is an Option<Box<dyn State>>, when we call as_ref, an Option<&Box<dyn State>> is returned. If we didn't call as_ref, we would get an error because we can't move state out of the borrowed &self of the function parameter.
We then call the unwrap method, which we know will never panic because we know the methods on Post ensure that state will always contain a Some value when those methods are done. This is one of the cases we talked about in "Cases in Which You Have More Information Than the Compiler" when we know that a None value is never possible, even though the compiler isn't able to understand that.
At this point, when we call content on the &Box<dyn State>, deref coercion will take effect on the & and the Box so the content method will ultimately be called on the type that implements the State trait. That means we need to add content to the State trait definition, and that is where we'll put the logic for what content to return depending on which state we have, as shown in Listing 17-18.
Filename: src/lib.rs
trait State {
--snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
1 ""
}
}
--snip--
struct Published {}
impl State for Published {
--snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
2 &post.content
}
}
Listing 17-18: Adding the content method to the State trait
We add a default implementation for the content method that returns an empty string slice [1]. That means we don't need to implement content on the Draft and PendingReview structs. The Published struct will override the content method and return the value in post.content [2].
Note that we need lifetime annotations on this method, as we discussed in Chapter 10. We're taking a reference to a post as an argument and returning a reference to part of that post, so the lifetime of the returned reference is related to the lifetime of the post argument.
And we're done---all of Listing 17-11 now works! We've implemented the state pattern with the rules of the blog post workflow. The logic related to the rules lives in the state objects rather than being scattered throughout Post.
Why Not An Enum?
You may have been wondering why we didn't use an enum with the different possible post states as variants. That's certainly a possible solution; try it and compare the end results to see which you prefer! One disadvantage of using an enum is that every place that checks the value of the enum will need a match expression or similar to handle every possible variant. This could get more repetitive than this trait object solution.