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.