]> git.proxmox.com Git - rustc.git/blame - src/doc/book/src/ch17-03-oo-design-patterns.md
New upstream version 1.48.0~beta.8+dfsg1
[rustc.git] / src / doc / book / src / ch17-03-oo-design-patterns.md
CommitLineData
13cf67c4
XL
1## Implementing an Object-Oriented Design Pattern
2
3The *state pattern* is an object-oriented design pattern. The crux of the
4pattern is that a value has some internal state, which is represented by a set
5of *state objects*, and the value’s behavior changes based on the internal
6state. The state objects share functionality: in Rust, of course, we use
7structs and traits rather than objects and inheritance. Each state object is
8responsible for its own behavior and for governing when it should change into
9another state. The value that holds a state object knows nothing about the
10different behavior of the states or when to transition between states.
11
12Using the state pattern means when the business requirements of the program
13change, we won’t need to change the code of the value holding the state or the
14code that uses the value. We’ll only need to update the code inside one of the
15state objects to change its rules or perhaps add more state objects. Let’s look
16at an example of the state design pattern and how to use it in Rust.
17
18We’ll implement a blog post workflow in an incremental way. The blog’s final
19functionality will look like this:
20
211. A blog post starts as an empty draft.
222. When the draft is done, a review of the post is requested.
233. When the post is approved, it gets published.
244. Only published blog posts return content to print, so unapproved posts can’t
25 accidentally be published.
26
27Any other changes attempted on a post should have no effect. For example, if we
28try to approve a draft blog post before we’ve requested a review, the post
29should remain an unpublished draft.
30
31Listing 17-11 shows this workflow in code form: this is an example usage of the
32API we’ll implement in a library crate named `blog`. This won’t compile yet
33because we haven’t implemented the `blog` crate yet.
34
35<span class="filename">Filename: src/main.rs</span>
36
74b04a01
XL
37```rust,ignore,does_not_compile
38{{#rustdoc_include ../listings/ch17-oop/listing-17-11/src/main.rs:all}}
13cf67c4
XL
39```
40
41<span class="caption">Listing 17-11: Code that demonstrates the desired
42behavior we want our `blog` crate to have</span>
43
44We want to allow the user to create a new draft blog post with `Post::new`.
45Then we want to allow text to be added to the blog post while it’s in the draft
46state. If we try to get the post’s content immediately, before approval,
47nothing should happen because the post is still a draft. We’ve added
48`assert_eq!` in the code for demonstration purposes. An excellent unit test for
49this would be to assert that a draft blog post returns an empty string from the
50`content` method, but we’re not going to write tests for this example.
51
52Next, we want to enable a request for a review of the post, and we want
53`content` to return an empty string while waiting for the review. When the post
54receives approval, it should get published, meaning the text of the post will
55be returned when `content` is called.
56
57Notice that the only type we’re interacting with from the crate is the `Post`
58type. This type will use the state pattern and will hold a value that will be
59one of three state objects representing the various states a post can be
60in—draft, waiting for review, or published. Changing from one state to another
61will be managed internally within the `Post` type. The states change in
62response to the methods called by our library’s users on the `Post` instance,
63but they don’t have to manage the state changes directly. Also, users can’t
64make a mistake with the states, like publishing a post before it’s reviewed.
65
66### Defining `Post` and Creating a New Instance in the Draft State
67
68Let’s get started on the implementation of the library! We know we need a
69public `Post` struct that holds some content, so we’ll start with the
70definition of the struct and an associated public `new` function to create an
71instance of `Post`, as shown in Listing 17-12. We’ll also make a private
72`State` trait. Then `Post` will hold a trait object of `Box<dyn State>`
73inside an `Option<T>` in a private field named `state`. You’ll see why the
74`Option<T>` is necessary in a bit.
75
76<span class="filename">Filename: src/lib.rs</span>
77
78```rust
74b04a01 79{{#rustdoc_include ../listings/ch17-oop/listing-17-12/src/lib.rs}}
13cf67c4
XL
80```
81
82<span class="caption">Listing 17-12: Definition of a `Post` struct and a `new`
83function that creates a new `Post` instance, a `State` trait, and a `Draft`
84struct</span>
85
86The `State` trait defines the behavior shared by different post states, and the
87`Draft`, `PendingReview`, and `Published` states will all implement the `State`
88trait. For now, the trait doesn’t have any methods, and we’ll start by defining
89just the `Draft` state because that is the state we want a post to start in.
90
91When we create a new `Post`, we set its `state` field to a `Some` value that
92holds a `Box`. This `Box` points to a new instance of the `Draft` struct. This
93ensures whenever we create a new instance of `Post`, it will start out as a
94draft. Because the `state` field of `Post` is private, there is no way to
95create a `Post` in any other state! In the `Post::new` function, we set the
96`content` field to a new, empty `String`.
97
98### Storing the Text of the Post Content
99
100Listing 17-11 showed that we want to be able to call a method named
101`add_text` and pass it a `&str` that is then added to the text content of the
102blog post. We implement this as a method rather than exposing the `content`
103field as `pub`. This means we can implement a method later that will control
104how the `content` field’s data is read. The `add_text` method is pretty
105straightforward, so let’s add the implementation in Listing 17-13 to the `impl
106Post` block:
107
108<span class="filename">Filename: src/lib.rs</span>
109
110```rust
74b04a01 111{{#rustdoc_include ../listings/ch17-oop/listing-17-13/src/lib.rs:here}}
13cf67c4
XL
112```
113
114<span class="caption">Listing 17-13: Implementing the `add_text` method to add
115text to a post’s `content`</span>
116
117The `add_text` method takes a mutable reference to `self`, because we’re
118changing the `Post` instance that we’re calling `add_text` on. We then call
119`push_str` on the `String` in `content` and pass the `text` argument to add to
120the saved `content`. This behavior doesn’t depend on the state the post is in,
121so it’s not part of the state pattern. The `add_text` method doesn’t interact
122with the `state` field at all, but it is part of the behavior we want to
123support.
124
125### Ensuring the Content of a Draft Post Is Empty
126
127Even after we’ve called `add_text` and added some content to our post, we still
128want the `content` method to return an empty string slice because the post is
532ac7d7 129still in the draft state, as shown on line 7 of Listing 17-11. For now, let’s
13cf67c4
XL
130implement the `content` method with the simplest thing that will fulfill this
131requirement: always returning an empty string slice. We’ll change this later
132once we implement the ability to change a post’s state so it can be published.
133So far, posts can only be in the draft state, so the post content should always
134be empty. Listing 17-14 shows this placeholder implementation:
135
136<span class="filename">Filename: src/lib.rs</span>
137
138```rust
74b04a01 139{{#rustdoc_include ../listings/ch17-oop/listing-17-14/src/lib.rs:here}}
13cf67c4
XL
140```
141
142<span class="caption">Listing 17-14: Adding a placeholder implementation for
143the `content` method on `Post` that always returns an empty string slice</span>
144
532ac7d7 145With this added `content` method, everything in Listing 17-11 up to line 7
13cf67c4
XL
146works as intended.
147
148### Requesting a Review of the Post Changes Its State
149
150Next, we need to add functionality to request a review of a post, which should
151change its state from `Draft` to `PendingReview`. Listing 17-15 shows this code:
152
153<span class="filename">Filename: src/lib.rs</span>
154
155```rust
74b04a01 156{{#rustdoc_include ../listings/ch17-oop/listing-17-15/src/lib.rs:here}}
13cf67c4
XL
157```
158
159<span class="caption">Listing 17-15: Implementing `request_review` methods on
160`Post` and the `State` trait</span>
161
162We give `Post` a public method named `request_review` that will take a mutable
163reference to `self`. Then we call an internal `request_review` method on the
164current state of `Post`, and this second `request_review` method consumes the
165current state and returns a new state.
166
167We’ve added the `request_review` method to the `State` trait; all types that
168implement the trait will now need to implement the `request_review` method.
169Note that rather than having `self`, `&self`, or `&mut self` as the first
170parameter of the method, we have `self: Box<Self>`. This syntax means the
171method is only valid when called on a `Box` holding the type. This syntax takes
172ownership of `Box<Self>`, invalidating the old state so the state value of the
173`Post` can transform into a new state.
174
175To consume the old state, the `request_review` method needs to take ownership
176of the state value. This is where the `Option` in the `state` field of `Post`
177comes in: we call the `take` method to take the `Some` value out of the `state`
178field and leave a `None` in its place, because Rust doesn’t let us have
179unpopulated fields in structs. This lets us move the `state` value out of
180`Post` rather than borrowing it. Then we’ll set the post’s `state` value to the
181result of this operation.
182
183We need to set `state` to `None` temporarily rather than setting it directly
184with code like `self.state = self.state.request_review();` to get ownership of
185the `state` value. This ensures `Post` can’t use the old `state` value after
186we’ve transformed it into a new state.
187
188The `request_review` method on `Draft` needs to return a new, boxed instance of
189a new `PendingReview` struct, which represents the state when a post is waiting
190for a review. The `PendingReview` struct also implements the `request_review`
191method but doesn’t do any transformations. Rather, it returns itself, because
192when we request a review on a post already in the `PendingReview` state, it
193should stay in the `PendingReview` state.
194
195Now we can start seeing the advantages of the state pattern: the
196`request_review` method on `Post` is the same no matter its `state` value. Each
197state is responsible for its own rules.
198
199We’ll leave the `content` method on `Post` as is, returning an empty string
200slice. We can now have a `Post` in the `PendingReview` state as well as in the
201`Draft` state, but we want the same behavior in the `PendingReview` state.
532ac7d7 202Listing 17-11 now works up to line 10!
13cf67c4
XL
203
204### Adding the `approve` Method that Changes the Behavior of `content`
205
206The `approve` method will be similar to the `request_review` method: it will
207set `state` to the value that the current state says it should have when that
208state is approved, as shown in Listing 17-16:
209
210<span class="filename">Filename: src/lib.rs</span>
211
212```rust
74b04a01 213{{#rustdoc_include ../listings/ch17-oop/listing-17-16/src/lib.rs:here}}
13cf67c4
XL
214```
215
216<span class="caption">Listing 17-16: Implementing the `approve` method on
217`Post` and the `State` trait</span>
218
219We add the `approve` method to the `State` trait and add a new struct that
220implements `State`, the `Published` state.
221
222Similar to `request_review`, if we call the `approve` method on a `Draft`, it
223will have no effect because it will return `self`. When we call `approve` on
224`PendingReview`, it returns a new, boxed instance of the `Published` struct.
225The `Published` struct implements the `State` trait, and for both the
226`request_review` method and the `approve` method, it returns itself, because
227the post should stay in the `Published` state in those cases.
228
229Now we need to update the `content` method on `Post`: if the state is
230`Published`, we want to return the value in the post’s `content` field;
231otherwise, we want to return an empty string slice, as shown in Listing 17-17:
232
233<span class="filename">Filename: src/lib.rs</span>
234
74b04a01
XL
235```rust,ignore,does_not_compile
236{{#rustdoc_include ../listings/ch17-oop/listing-17-17/src/lib.rs:here}}
13cf67c4
XL
237```
238
239<span class="caption">Listing 17-17: Updating the `content` method on `Post` to
240delegate to a `content` method on `State`</span>
241
242Because the goal is to keep all these rules inside the structs that implement
243`State`, we call a `content` method on the value in `state` and pass the post
244instance (that is, `self`) as an argument. Then we return the value that is
245returned from using the `content` method on the `state` value.
246
247We call the `as_ref` method on the `Option` because we want a reference to the
248value inside the `Option` rather than ownership of the value. Because `state`
249is an `Option<Box<dyn State>>`, when we call `as_ref`, an `Option<&Box<dyn State>>` is
250returned. If we didn’t call `as_ref`, we would get an error because we can’t
251move `state` out of the borrowed `&self` of the function parameter.
252
253We then call the `unwrap` method, which we know will never panic, because we
254know the methods on `Post` ensure that `state` will always contain a `Some`
255value when those methods are done. This is one of the cases we talked about in
9fa01778
XL
256the [“Cases In Which You Have More Information Than the
257Compiler”][more-info-than-rustc]<!-- ignore --> section of Chapter 9 when we
258know that a `None` value is never possible, even though the compiler isn’t able
259to understand that.
13cf67c4
XL
260
261At this point, when we call `content` on the `&Box<dyn State>`, deref coercion will
262take effect on the `&` and the `Box` so the `content` method will ultimately be
263called on the type that implements the `State` trait. That means we need to add
264`content` to the `State` trait definition, and that is where we’ll put the
265logic for what content to return depending on which state we have, as shown in
266Listing 17-18:
267
268<span class="filename">Filename: src/lib.rs</span>
269
270```rust
74b04a01 271{{#rustdoc_include ../listings/ch17-oop/listing-17-18/src/lib.rs:here}}
13cf67c4
XL
272```
273
274<span class="caption">Listing 17-18: Adding the `content` method to the `State`
275trait</span>
276
277We add a default implementation for the `content` method that returns an empty
278string slice. That means we don’t need to implement `content` on the `Draft`
279and `PendingReview` structs. The `Published` struct will override the `content`
280method and return the value in `post.content`.
281
282Note that we need lifetime annotations on this method, as we discussed in
283Chapter 10. We’re taking a reference to a `post` as an argument and returning a
284reference to part of that `post`, so the lifetime of the returned reference is
285related to the lifetime of the `post` argument.
286
287And we’re done—all of Listing 17-11 now works! We’ve implemented the state
288pattern with the rules of the blog post workflow. The logic related to the
289rules lives in the state objects rather than being scattered throughout `Post`.
290
291### Trade-offs of the State Pattern
292
293We’ve shown that Rust is capable of implementing the object-oriented state
294pattern to encapsulate the different kinds of behavior a post should have in
295each state. The methods on `Post` know nothing about the various behaviors. The
296way we organized the code, we have to look in only one place to know the
297different ways a published post can behave: the implementation of the `State`
298trait on the `Published` struct.
299
300If we were to create an alternative implementation that didn’t use the state
301pattern, we might instead use `match` expressions in the methods on `Post` or
302even in the `main` code that checks the state of the post and changes behavior
303in those places. That would mean we would have to look in several places to
304understand all the implications of a post being in the published state! This
305would only increase the more states we added: each of those `match` expressions
306would need another arm.
307
308With the state pattern, the `Post` methods and the places we use `Post` don’t
309need `match` expressions, and to add a new state, we would only need to add a
310new struct and implement the trait methods on that one struct.
311
312The implementation using the state pattern is easy to extend to add more
313functionality. To see the simplicity of maintaining code that uses the state
314pattern, try a few of these suggestions:
315
316* Add a `reject` method that changes the post’s state from `PendingReview` back
317 to `Draft`.
318* Require two calls to `approve` before the state can be changed to `Published`.
319* Allow users to add text content only when a post is in the `Draft` state.
320 Hint: have the state object responsible for what might change about the
321 content but not responsible for modifying the `Post`.
322
323One downside of the state pattern is that, because the states implement the
324transitions between states, some of the states are coupled to each other. If we
325add another state between `PendingReview` and `Published`, such as `Scheduled`,
326we would have to change the code in `PendingReview` to transition to
327`Scheduled` instead. It would be less work if `PendingReview` didn’t need to
328change with the addition of a new state, but that would mean switching to
329another design pattern.
330
331Another downside is that we’ve duplicated some logic. To eliminate some of the
332duplication, we might try to make default implementations for the
333`request_review` and `approve` methods on the `State` trait that return `self`;
334however, this would violate object safety, because the trait doesn’t know what
335the concrete `self` will be exactly. We want to be able to use `State` as a
336trait object, so we need its methods to be object safe.
337
338Other duplication includes the similar implementations of the `request_review`
339and `approve` methods on `Post`. Both methods delegate to the implementation of
340the same method on the value in the `state` field of `Option` and set the new
341value of the `state` field to the result. If we had a lot of methods on `Post`
342that followed this pattern, we might consider defining a macro to eliminate the
9fa01778 343repetition (see the [“Macros”][macros]<!-- ignore --> section in Chapter 19).
13cf67c4
XL
344
345By implementing the state pattern exactly as it’s defined for object-oriented
346languages, we’re not taking as full advantage of Rust’s strengths as we could.
347Let’s look at some changes we can make to the `blog` crate that can make
348invalid states and transitions into compile time errors.
349
350#### Encoding States and Behavior as Types
351
352We’ll show you how to rethink the state pattern to get a different set of
353trade-offs. Rather than encapsulating the states and transitions completely so
354outside code has no knowledge of them, we’ll encode the states into different
355types. Consequently, Rust’s type checking system will prevent attempts to use
356draft posts where only published posts are allowed by issuing a compiler error.
357
358Let’s consider the first part of `main` in Listing 17-11:
359
360<span class="filename">Filename: src/main.rs</span>
361
362```rust,ignore
74b04a01 363{{#rustdoc_include ../listings/ch17-oop/listing-17-11/src/main.rs:here}}
13cf67c4
XL
364```
365
366We still enable the creation of new posts in the draft state using `Post::new`
367and the ability to add text to the post’s content. But instead of having a
368`content` method on a draft post that returns an empty string, we’ll make it so
369draft posts don’t have the `content` method at all. That way, if we try to get
370a draft post’s content, we’ll get a compiler error telling us the method
371doesn’t exist. As a result, it will be impossible for us to accidentally
372display draft post content in production, because that code won’t even compile.
373Listing 17-19 shows the definition of a `Post` struct and a `DraftPost` struct,
374as well as methods on each:
375
376<span class="filename">Filename: src/lib.rs</span>
377
378```rust
74b04a01 379{{#rustdoc_include ../listings/ch17-oop/listing-17-19/src/lib.rs}}
13cf67c4
XL
380```
381
382<span class="caption">Listing 17-19: A `Post` with a `content` method and a
383`DraftPost` without a `content` method</span>
384
385Both the `Post` and `DraftPost` structs have a private `content` field that
386stores the blog post text. The structs no longer have the `state` field because
387we’re moving the encoding of the state to the types of the structs. The `Post`
388struct will represent a published post, and it has a `content` method that
389returns the `content`.
390
391We still have a `Post::new` function, but instead of returning an instance of
392`Post`, it returns an instance of `DraftPost`. Because `content` is private
393and there aren’t any functions that return `Post`, it’s not possible to create
394an instance of `Post` right now.
395
396The `DraftPost` struct has an `add_text` method, so we can add text to
397`content` as before, but note that `DraftPost` does not have a `content` method
398defined! So now the program ensures all posts start as draft posts, and draft
399posts don’t have their content available for display. Any attempt to get around
400these constraints will result in a compiler error.
401
402#### Implementing Transitions as Transformations into Different Types
403
404So how do we get a published post? We want to enforce the rule that a draft
405post has to be reviewed and approved before it can be published. A post in the
406pending review state should still not display any content. Let’s implement
407these constraints by adding another struct, `PendingReviewPost`, defining the
408`request_review` method on `DraftPost` to return a `PendingReviewPost`, and
409defining an `approve` method on `PendingReviewPost` to return a `Post`, as
410shown in Listing 17-20:
411
412<span class="filename">Filename: src/lib.rs</span>
413
414```rust
74b04a01 415{{#rustdoc_include ../listings/ch17-oop/listing-17-20/src/lib.rs:here}}
13cf67c4
XL
416```
417
418<span class="caption">Listing 17-20: A `PendingReviewPost` that gets created by
419calling `request_review` on `DraftPost` and an `approve` method that turns a
420`PendingReviewPost` into a published `Post`</span>
421
422The `request_review` and `approve` methods take ownership of `self`, thus
423consuming the `DraftPost` and `PendingReviewPost` instances and transforming
424them into a `PendingReviewPost` and a published `Post`, respectively. This way,
425we won’t have any lingering `DraftPost` instances after we’ve called
426`request_review` on them, and so forth. The `PendingReviewPost` struct doesn’t
427have a `content` method defined on it, so attempting to read its content
428results in a compiler error, as with `DraftPost`. Because the only way to get a
429published `Post` instance that does have a `content` method defined is to call
430the `approve` method on a `PendingReviewPost`, and the only way to get a
431`PendingReviewPost` is to call the `request_review` method on a `DraftPost`,
432we’ve now encoded the blog post workflow into the type system.
433
434But we also have to make some small changes to `main`. The `request_review` and
435`approve` methods return new instances rather than modifying the struct they’re
436called on, so we need to add more `let post =` shadowing assignments to save
437the returned instances. We also can’t have the assertions about the draft and
438pending review post’s contents be empty strings, nor do we need them: we can’t
439compile code that tries to use the content of posts in those states any longer.
440The updated code in `main` is shown in Listing 17-21:
441
442<span class="filename">Filename: src/main.rs</span>
443
444```rust,ignore
74b04a01 445{{#rustdoc_include ../listings/ch17-oop/listing-17-21/src/main.rs}}
13cf67c4
XL
446```
447
448<span class="caption">Listing 17-21: Modifications to `main` to use the new
449implementation of the blog post workflow</span>
450
451The changes we needed to make to `main` to reassign `post` mean that this
452implementation doesn’t quite follow the object-oriented state pattern anymore:
453the transformations between the states are no longer encapsulated entirely
454within the `Post` implementation. However, our gain is that invalid states are
455now impossible because of the type system and the type checking that happens at
456compile time! This ensures that certain bugs, such as display of the content of
457an unpublished post, will be discovered before they make it to production.
458
459Try the tasks suggested for additional requirements that we mentioned at the
460start of this section on the `blog` crate as it is after Listing 17-20 to see
461what you think about the design of this version of the code. Note that some of
462the tasks might be completed already in this design.
463
464We’ve seen that even though Rust is capable of implementing object-oriented
465design patterns, other patterns, such as encoding state into the type system,
466are also available in Rust. These patterns have different trade-offs. Although
467you might be very familiar with object-oriented patterns, rethinking the
468problem to take advantage of Rust’s features can provide benefits, such as
469preventing some bugs at compile time. Object-oriented patterns won’t always be
470the best solution in Rust due to certain features, like ownership, that
471object-oriented languages don’t have.
472
473## Summary
474
475No matter whether or not you think Rust is an object-oriented language after
476reading this chapter, you now know that you can use trait objects to get some
477object-oriented features in Rust. Dynamic dispatch can give your code some
478flexibility in exchange for a bit of runtime performance. You can use this
479flexibility to implement object-oriented patterns that can help your code’s
480maintainability. Rust also has other features, like ownership, that
481object-oriented languages don’t have. An object-oriented pattern won’t always
482be the best way to take advantage of Rust’s strengths, but is an available
483option.
484
485Next, we’ll look at patterns, which are another of Rust’s features that enable
486lots of flexibility. We’ve looked at them briefly throughout the book but
487haven’t seen their full capability yet. Let’s go!
9fa01778
XL
488
489[more-info-than-rustc]: ch09-03-to-panic-or-not-to-panic.html#cases-in-which-you-have-more-information-than-the-compiler
490[macros]: ch19-06-macros.html#macros