mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 04:54:17 -06:00
75% done ch18.3
This commit is contained in:
parent
070680441b
commit
35fda43f47
@ -239,3 +239,302 @@ Then we will set the post's `state` value to the result of this operation.
|
||||
We need to set `state` to `None` temporarily rather than setting it directly with something like `self.state = self.state.request_review();` to get ownership of the `state` value.
|
||||
|
||||
This ensures that `Post` can't use the old `state` value after we transformed it into a new state.
|
||||
|
||||
|
||||
The `request_review` method on `Draft` returns a new boxed instance of a new `PendingReview` struct.
|
||||
|
||||
This represents the state when a post is waiting for a review.
|
||||
|
||||
The `PendingReview` struct also implements the `request_review` method but doesn't do any transformations.
|
||||
|
||||
It instead returns itself, because when we request a review on a post already in the `PendingReview` state, it should stay in the `PendingReview` state.
|
||||
|
||||
Now the advantages of the state pattern are staring to be seen: the `request_review` method on `Post` is the same no matter its `state` value.
|
||||
|
||||
Each state is responsible for its own rules.
|
||||
|
||||
We leave the `content` method on `Post` as is, returning an empty string slice.
|
||||
|
||||
We can now have a `Post` in the `PendingReview` state as well as in the `Draft` state, but we want the same behavior in the `PendingReview` state.
|
||||
|
||||
## 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.
|
||||
|
||||
Here is the new code
|
||||
```rust
|
||||
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> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingReview {}
|
||||
|
||||
impl State for PendingReview {
|
||||
// --snip--
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
Here we added the `spprove` method to the `State` trait and add a new struct that implements `State`, the `Published` state.
|
||||
|
||||
Similar to how `request_review` on `PendingReview` works, if we call the `approve` method on a `Draft`, it will have no effect because `approve` will return `self`.
|
||||
|
||||
When we call `approve` on `PendingReview`, it returns a new boxed instance of the `Published` struct.
|
||||
|
||||
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.
|
||||
|
||||
We now need a way to update the `content` method on `Post`.
|
||||
|
||||
We want the value returned from `content` to depend on the current state of `Post`, so we are going to have the `Post` delegate to `cotnent` method defined on its `state`.
|
||||
|
||||
Here is the code for this
|
||||
```rust
|
||||
impl Post {
|
||||
// --snip--
|
||||
pub fn content(&self) -> &str {
|
||||
self.state.as_ref().unwrap().content(self)
|
||||
}
|
||||
// --snip--
|
||||
}
|
||||
```
|
||||
The goal is to keep all the 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 is returned from using the `content` method on the `state` value.
|
||||
|
||||
As 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.
|
||||
|
||||
Then we call the `unwrap` method, we know this will never panic.
|
||||
|
||||
We know the methods on `Post` ensure that `state` will always contain a `Some` value when those methods are done.
|
||||
|
||||
This is a case where we have more information than the compiler (previously discussed in [Ch 9]()) when we know that a `None` value is never possible, even though the compiler isn't able to understand that.
|
||||
|
||||
Now 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.
|
||||
|
||||
This means we need to add `content` to the `State` trait definition, and that is where we will put the logic for what content to return depending on which state we have.
|
||||
|
||||
Here is that addition
|
||||
```rust
|
||||
trait State {
|
||||
// --snip--
|
||||
fn content<'a>(&self, post: &'a Post) -> &'a str {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
// --snip--
|
||||
struct Published {}
|
||||
|
||||
impl State for Published {
|
||||
// --snip--
|
||||
fn content<'a>(&self, post: &'a Post) -> &'a str {
|
||||
&post.content
|
||||
}
|
||||
}
|
||||
```
|
||||
Here we added a default implementation for the `content` method that returns an empty string slice.
|
||||
|
||||
This means we don't need to implement `cotent` on the `Draft` and `PendingReview` structs.
|
||||
|
||||
The `Published` struct will override the `content` method and return the value in `post.content`.
|
||||
|
||||
Note that we need a lifetime annotation on this method.
|
||||
|
||||
Here we are 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.
|
||||
|
||||
We have finally 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`.
|
||||
|
||||
Final Code:
|
||||
```rust
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait State {
|
||||
// --snip--
|
||||
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 {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
// --snip--
|
||||
|
||||
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 {
|
||||
// --snip--
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
### Why Not An Enum?
|
||||
You may wonder why we didn't use an `enum` with the different possible post states as variants.
|
||||
|
||||
This is a possible solution, you have to try it and compare the end results to see which is preferred.
|
||||
|
||||
One disadvantage of using an enum is 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.
|
||||
|
||||
## Trade-offs of the State Pattern
|
||||
Here we have shown that Rust is capable of implementing the object-oriented state pattern to encapsulate the different kinds of behavior a post should have in each state.
|
||||
|
||||
The methods on `Post` know nothing about the various behaviors.
|
||||
|
||||
The way in which code is organized, we have to look only in one place to know the different ways a published post can behave: the implementation of the `State` trait on the `Published` struct.
|
||||
|
||||
If we were to create an alternative implementation that didn't use the state pattern, we might instead use `match` expression in the `Post` or even in the `main` code.
|
||||
|
||||
This would check for the state of the post and changes behavior ion those places.
|
||||
|
||||
That means we would have to look in several places to understand all the implications of a post being in the published state.
|
||||
|
||||
This would only increase the more states we added: each of those `match` expressions would need another arm.
|
||||
|
||||
With the state pattern, the `Post` methods and the places we use `Post` don't need `match` expressions and to add a new state.
|
||||
|
||||
We would only need to add a new struct and implement the trait methods on that one struct.
|
||||
|
||||
The implementation using the state pattern is easy to extend to add more functionality.
|
||||
|
||||
To highlight the simplicity of maintaining code that uses the state pattern, try a few of these suggestions:
|
||||
- Add a `reject` method that changes the post's state from `PendingReview` back to `Draft`.
|
||||
- Require two calls to approve before the state can be changed to `Published`.
|
||||
- Allow users to add text content only when a post is in the `Draft` state.
|
||||
- Hint: have the state object responsible for what might change about the content but not responsible for modifying the `Post`.
|
||||
One downside of the state pattern is that, because the states implement the transitions between states, some of the states are coupled to each other.
|
||||
|
||||
If we add another state between `PendingReview` and `Published`, such as `Scheduled`, we would have to change the code in `PendingReview` to transitioned to `Scheduled` instead.
|
||||
|
||||
|
||||
It would be less work if `PendingReview` didn't need to change with the addition of a new state, but that would mean switching to another design pattern.
|
||||
|
||||
Another downside is that we have dupliced some logic.
|
||||
|
||||
In order to eliminate some of the duplication, we may try to make default implementations for the `request_review` and `approve` methods on the `State` trait that return `self`
|
||||
|
||||
However, this would not be dyn compatible.
|
||||
|
||||
This is because the trait doesn't know what the concrete `self` will be exactly.
|
||||
|
||||
We want to be able to use `State` as a trait object so we need its methods to be dyn compatible.
|
||||
|
||||
Other duplication includes the similar implementations of the `request_review` and `approve` methods on `Post`.
|
||||
|
||||
Both methods delegate to the implementation of the same method on the value in the `state` field of `Option` and set the new value of the `state` field to the result.
|
||||
|
||||
|
||||
If we had a lot of methods on `Post` that followed this pattern, we may consider defining a macro to eliminate the repetition (This will be discussed in Ch20).
|
||||
|
||||
By implementing the state pattern exactly as it is defined for object-oriented languages, we are not taking full advantage of Rust's strengths as we could.
|
||||
|
||||
Now lets look at some changes to make the `blog` crate that can make invalid states and transitions into compile time errors.
|
||||
|
||||
## Encoding States and Behavior as Types
|
||||
|
Loading…
x
Reference in New Issue
Block a user