mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 04:54:17 -06:00
finished ch18.3 and ch18
This commit is contained in:
parent
35fda43f47
commit
849d24a10b
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",
|
||||
|
@ -538,3 +538,167 @@ By implementing the state pattern exactly as it is defined for object-oriented l
|
||||
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
|
Loading…
x
Reference in New Issue
Block a user