From 35fda43f47b1b3f225eb79962d13c019e4a764dc Mon Sep 17 00:00:00 2001 From: darkicewolf50 Date: Mon, 7 Apr 2025 14:35:07 -0600 Subject: [PATCH] 75% done ch18.3 --- Implementing OO Design Pattern.md | 299 ++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) diff --git a/Implementing OO Design Pattern.md b/Implementing OO Design Pattern.md index a27f8d4..c884fc6 100644 --- a/Implementing OO Design Pattern.md +++ b/Implementing OO Design Pattern.md @@ -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) -> Box; + fn approve(self: Box) -> Box; +} + +struct Draft {} + +impl State for Draft { + // --snip-- + fn approve(self: Box) -> Box { + self + } +} + +struct PendingReview {} + +impl State for PendingReview { + // --snip-- + fn approve(self: Box) -> Box { + Box::new(Published {}) + } +} + +struct Published {} + +impl State for Published { + fn request_review(self: Box) -> Box { + self + } + + fn approve(self: Box) -> Box { + 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>`, when we call `as_ref`, an `Option<&Box>` 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`, 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>, + 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) -> Box; + fn approve(self: Box) -> Box; + + fn content<'a>(&self, post: &'a Post) -> &'a str { + "" + } +} + +// --snip-- + +struct Draft {} + +impl State for Draft { + fn request_review(self: Box) -> Box { + Box::new(PendingReview {}) + } + + fn approve(self: Box) -> Box { + self + } +} + +struct PendingReview {} + +impl State for PendingReview { + fn request_review(self: Box) -> Box { + self + } + + fn approve(self: Box) -> Box { + Box::new(Published {}) + } +} + +struct Published {} + +impl State for Published { + // --snip-- + fn request_review(self: Box) -> Box { + self + } + + fn approve(self: Box) -> Box { + 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