Compare commits

...

2 Commits

Author SHA1 Message Date
849d24a10b finished ch18.3 and ch18
All checks were successful
Test Gitea Actions / first (push) Successful in 22s
Test Gitea Actions / check-code (push) Successful in 17s
Test Gitea Actions / test (push) Successful in 16s
Test Gitea Actions / documentation-check (push) Successful in 16s
2025-04-07 16:56:06 -06:00
35fda43f47 75% done ch18.3 2025-04-07 14:35:07 -06:00
3 changed files with 482 additions and 4 deletions

View File

@ -35,6 +35,20 @@
"title": "Implementing OO Design Pattern"
}
},
{
"id": "629d55df46f486d8",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "Pattern Matching.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "Pattern Matching"
}
},
{
"id": "6ed9f182aa3d5f3e",
"type": "leaf",
@ -46,7 +60,7 @@
}
}
],
"currentTab": 1
"currentTab": 2
}
],
"direction": "vertical"
@ -189,10 +203,11 @@
"command-palette:Open command palette": false
}
},
"active": "f9ef446f856cead7",
"active": "629d55df46f486d8",
"lastOpenFiles": [
"Trait Objects that allow for Values of Different Types.md",
"Implementing OO Design Pattern.md",
"Pattern Matching.md",
"Trait Objects that allow for Values of Different Types.md",
"Characteristics of OO Languages.md",
"OOP Programming Features.md",
"Futures, Tasks and Threads Together.md",
@ -216,7 +231,6 @@
"Improving The IO Project.md",
"Tests.md",
"The Preformance Closures and Iterators.md",
"minigrep/README.md",
"minigrep/src/lib.rs",
"does_not_compile.svg",
"Untitled.canvas",

View File

@ -239,3 +239,466 @@ 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
We will show how you can rethink the state pattern to get a different set of trade-offs.
Rather than encapsulating the states and transitions completely so outside code has no knowledge of them, we will encode the states into different types.
Rust's type checking system will prevent attempts to use draft posts where only published posts are allowed by issuing a compiler error.
Lets consider this first part of `main` from before
```rust
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
}
```
We still need to enable the creation of new posts in the draft state using `Post::new` and the ability to add text to the post's content.
Instead of having a `content` method on a draft post that returns an empty string, we will make it so draft posts don't have the `content` method at all.
This way if we try to get a draft post's content, we will get a compiler error telling us the method doesn't exist.
This results in being impossible for us to accidentally display draft post content in production, because that code won't even compile.
Here is the definition of a `Post` struct and a `DraftPost` struct as well as methods on each.
```rust
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
```
Both the `Post` and `DraftPost` structs have a private `content` field that stores the blog post text.
The structs no longer have the `state` field because we are moving the encoding of that state to the types of structs.
The `Post` struct will represent a published post, and it has a `content` method that returns the `content`.
We still have a `Post::new` function, but instead of returning an instance of `Post`, it returns an instance of `DraftPost`.
Due to `content` being private and there aren't any functions that return `Post`, it is not possible to create an instance of `Post` right now.
The `DraftPost` struct has an `add_text`method, so we can add text to `content` as before.
Note that `DraftPost` does not have a `content` method defined.
So now the program ensures all posts start as draft posts, and draft posts don't have their content available for display.
Any attempt to get around these constraints will result in a compiler error.
## Implementing Transitions as Transformations into Different Types
How do we get a published post?
We want to enforce the rule that a draft post has to be reviewed and approved before it can be published.
A post in the pending review state should still not display any content.
We will implement these constraints by adding another struct, `PendingReviewPost`.
We will define the `request_review` method on `DraftPost` to return a `PendingReviewPost`.
Finally we will define an `approve` method on `PendingReviewPost` to return a `Post`.
Here is the code implementation.
```rust
impl DraftPost {
// --snip--
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
```
Here the `request_review` and `approve` methods take ownership of `self`.
This thus consumes the `DraftPost` and `PendingReviewPost` instances and transforming them into a `PendingReviewPost` and a published `Post`.
This way we will not have any lingering `DraftPost` instances after we called `request_review` on them and so on.
The `PendingReviewPost` struct also doesn't have a `content` method defined on it.
Again attempting to read its content results in a compiler error.
Because the only way to get a published `Post` instance that does have a `content` method defined is to call the `approve` method on a `PendingReviewPost`, and the only way to get a `PendingReviewPost` is to call the `request_review` method on a `DraftPost`.
Now we have encoded the blog post workflow into the type system.
We also have to make some changes to `main`.
The `reequest_review` and `approve`methods return new instances rather than modifying the struct they are called on.
We need to add more `let post =` shadowing assignments to save the returned instances.
We also can't have assertions about the draft and pending review posts' contents being empty strings, nor do we need them.
We are unable to compile any code that tires to use the content of posts in those states any longer.
Here is the updated code in `main`
```rust
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
```
The changes we need to make to `main` to reassign `post` mean that this implementation doesn't quite follow the object oriented state pattern anymore.
The transformations between the states are no longer encapsulated entirely within the `Post` implementation.
However our gain is that invalid states are now impossible because of the type system and the type checking that happens at compile time.
This enforces that certain bugs, such as display of the content of an unpublished post, will be discovered before they make it production.
Try the tasks suggested before on the `blog` crate as it is after to see what you think about the design of this version of the code.
Note that some of the tasks might be completed already in this design.
We have seen that even though Rust is capable of implementing object-oriented design patterns, other patterns such as encoding state into the type system, are also available in Rust.
These patterns have different trade0ffs.
While you may be very familiar with object-oriented patterns, rethinking the problem to take advantage of Rust's features can provide benefits, such as preventing some bugs due to certain features, like ownership, that object-oriented languages don't have.

1
Pattern Matching.md Normal file
View File

@ -0,0 +1 @@
# Patterns and Matching