mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-08-01 07:40:54 -06:00
Compare commits
2 Commits
070680441b
...
849d24a10b
Author | SHA1 | Date | |
---|---|---|---|
849d24a10b | |||
35fda43f47 |
22
.obsidian/workspace.json
vendored
22
.obsidian/workspace.json
vendored
@ -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",
|
||||
|
@ -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
1
Pattern Matching.md
Normal file
@ -0,0 +1 @@
|
||||
# Patterns and Matching
|
Reference in New Issue
Block a user