]>
Commit | Line | Data |
---|---|---|
5099ac24 FG |
1 | <!-- DO NOT EDIT THIS FILE. |
2 | ||
3 | This file is periodically generated from the content in the `/src/` | |
4 | directory, so all fixes need to be made in `/src/`. | |
5 | --> | |
6 | ||
7 | [TOC] | |
8 | ||
923072b8 FG |
9 | # Object-Oriented Programming Features of Rust |
10 | ||
11 | Object-oriented programming (OOP) is a way of modeling programs. Objects as a | |
12 | programmatic concept were introduced in the programming language Simula in the | |
13 | 1960s. Those objects influenced Alan Kay’s programming architecture in which | |
14 | objects pass messages to each other. To describe this architecture, he coined | |
15 | the term *object-oriented programming* in 1967. Many competing definitions | |
16 | describe what OOP is, and by some of these definitions Rust is object-oriented, | |
17 | but by others it is not. In this chapter, we’ll explore certain characteristics | |
18 | that are commonly considered object-oriented and how those characteristics | |
19 | translate to idiomatic Rust. We’ll then show you how to implement an | |
20 | object-oriented design pattern in Rust and discuss the trade-offs of doing so | |
21 | versus implementing a solution using some of Rust’s strengths instead. | |
22 | ||
23 | <!-- Nit: we should probably use "object-oriented" throughout, rather using both | |
24 | "object-oriented" and "object oriented" | |
25 | /JT --> | |
26 | <!-- Done! /Carol --> | |
5099ac24 FG |
27 | |
28 | ## Characteristics of Object-Oriented Languages | |
29 | ||
30 | There is no consensus in the programming community about what features a | |
923072b8 | 31 | language must have to be considered object-oriented. Rust is influenced by many |
5099ac24 FG |
32 | programming paradigms, including OOP; for example, we explored the features |
33 | that came from functional programming in Chapter 13. Arguably, OOP languages | |
34 | share certain common characteristics, namely objects, encapsulation, and | |
35 | inheritance. Let’s look at what each of those characteristics means and whether | |
36 | Rust supports it. | |
37 | ||
38 | ### Objects Contain Data and Behavior | |
39 | ||
40 | The book *Design Patterns: Elements of Reusable Object-Oriented Software* by | |
41 | Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley | |
42 | Professional, 1994), colloquially referred to as *The Gang of Four* book, is a | |
43 | catalog of object-oriented design patterns. It defines OOP this way: | |
44 | ||
45 | > Object-oriented programs are made up of objects. An *object* packages both | |
46 | > data and the procedures that operate on that data. The procedures are | |
47 | > typically called *methods* or *operations*. | |
48 | ||
923072b8 | 49 | Using this definition, Rust is object-oriented: structs and enums have data, |
5099ac24 FG |
50 | and `impl` blocks provide methods on structs and enums. Even though structs and |
51 | enums with methods aren’t *called* objects, they provide the same | |
52 | functionality, according to the Gang of Four’s definition of objects. | |
53 | ||
54 | ### Encapsulation that Hides Implementation Details | |
55 | ||
56 | Another aspect commonly associated with OOP is the idea of *encapsulation*, | |
57 | which means that the implementation details of an object aren’t accessible to | |
58 | code using that object. Therefore, the only way to interact with an object is | |
59 | through its public API; code using the object shouldn’t be able to reach into | |
60 | the object’s internals and change data or behavior directly. This enables the | |
61 | programmer to change and refactor an object’s internals without needing to | |
62 | change the code that uses the object. | |
63 | ||
64 | We discussed how to control encapsulation in Chapter 7: we can use the `pub` | |
65 | keyword to decide which modules, types, functions, and methods in our code | |
66 | should be public, and by default everything else is private. For example, we | |
67 | can define a struct `AveragedCollection` that has a field containing a vector | |
68 | of `i32` values. The struct can also have a field that contains the average of | |
69 | the values in the vector, meaning the average doesn’t have to be computed | |
70 | on demand whenever anyone needs it. In other words, `AveragedCollection` will | |
71 | cache the calculated average for us. Listing 17-1 has the definition of the | |
72 | `AveragedCollection` struct: | |
73 | ||
74 | Filename: src/lib.rs | |
75 | ||
76 | ``` | |
77 | pub struct AveragedCollection { | |
78 | list: Vec<i32>, | |
79 | average: f64, | |
80 | } | |
81 | ``` | |
82 | ||
83 | Listing 17-1: An `AveragedCollection` struct that maintains a list of integers | |
84 | and the average of the items in the collection | |
85 | ||
86 | The struct is marked `pub` so that other code can use it, but the fields within | |
87 | the struct remain private. This is important in this case because we want to | |
88 | ensure that whenever a value is added or removed from the list, the average is | |
89 | also updated. We do this by implementing `add`, `remove`, and `average` methods | |
90 | on the struct, as shown in Listing 17-2: | |
91 | ||
92 | Filename: src/lib.rs | |
93 | ||
94 | ``` | |
95 | impl AveragedCollection { | |
96 | pub fn add(&mut self, value: i32) { | |
97 | self.list.push(value); | |
98 | self.update_average(); | |
99 | } | |
100 | ||
101 | pub fn remove(&mut self) -> Option<i32> { | |
102 | let result = self.list.pop(); | |
103 | match result { | |
104 | Some(value) => { | |
105 | self.update_average(); | |
106 | Some(value) | |
107 | } | |
108 | None => None, | |
109 | } | |
110 | } | |
111 | ||
112 | pub fn average(&self) -> f64 { | |
113 | self.average | |
114 | } | |
115 | ||
116 | fn update_average(&mut self) { | |
117 | let total: i32 = self.list.iter().sum(); | |
118 | self.average = total as f64 / self.list.len() as f64; | |
119 | } | |
120 | } | |
121 | ``` | |
122 | ||
923072b8 FG |
123 | <!-- The above example will crash with a division by zero if you call it at |
124 | any time when it's empty. Not sure if we want to fix, but thought I'd point | |
125 | it out. | |
126 | /JT --> | |
127 | <!-- It actually won't because f64 / 0 is NaN, not a panic /Carol --> | |
128 | ||
5099ac24 FG |
129 | Listing 17-2: Implementations of the public methods `add`, `remove`, and |
130 | `average` on `AveragedCollection` | |
131 | ||
132 | The public methods `add`, `remove`, and `average` are the only ways to access | |
133 | or modify data in an instance of `AveragedCollection`. When an item is added | |
134 | to `list` using the `add` method or removed using the `remove` method, the | |
135 | implementations of each call the private `update_average` method that handles | |
136 | updating the `average` field as well. | |
137 | ||
138 | We leave the `list` and `average` fields private so there is no way for | |
923072b8 FG |
139 | external code to add or remove items to or from the `list` field directly; |
140 | otherwise, the `average` field might become out of sync when the `list` | |
141 | changes. The `average` method returns the value in the `average` field, | |
142 | allowing external code to read the `average` but not modify it. | |
5099ac24 FG |
143 | |
144 | Because we’ve encapsulated the implementation details of the struct | |
145 | `AveragedCollection`, we can easily change aspects, such as the data structure, | |
146 | in the future. For instance, we could use a `HashSet<i32>` instead of a | |
147 | `Vec<i32>` for the `list` field. As long as the signatures of the `add`, | |
148 | `remove`, and `average` public methods stay the same, code using | |
149 | `AveragedCollection` wouldn’t need to change. If we made `list` public instead, | |
150 | this wouldn’t necessarily be the case: `HashSet<i32>` and `Vec<i32>` have | |
151 | different methods for adding and removing items, so the external code would | |
152 | likely have to change if it were modifying `list` directly. | |
153 | ||
923072b8 FG |
154 | If encapsulation is a required aspect for a language to be considered |
155 | object-oriented, then Rust meets that requirement. The option to use `pub` or | |
156 | not for different parts of code enables encapsulation of implementation details. | |
5099ac24 FG |
157 | |
158 | ### Inheritance as a Type System and as Code Sharing | |
159 | ||
923072b8 FG |
160 | *Inheritance* is a mechanism whereby an object can inherit elements from |
161 | another object’s definition, thus gaining the parent object’s data and behavior | |
162 | without you having to define them again. | |
5099ac24 FG |
163 | |
164 | If a language must have inheritance to be an object-oriented language, then | |
165 | Rust is not one. There is no way to define a struct that inherits the parent | |
923072b8 FG |
166 | struct’s fields and method implementations without using a macro. |
167 | ||
168 | However, if you’re used to having inheritance in your programming toolbox, you | |
169 | can use other solutions in Rust, depending on your reason for reaching for | |
170 | inheritance in the first place. | |
171 | ||
172 | You would choose inheritance for two main reasons. One is for reuse of code: | |
173 | you can implement particular behavior for one type, and inheritance enables you | |
174 | to reuse that implementation for a different type. You can do this in a limited | |
175 | way in Rust code using default trait method implementations, which you saw in | |
176 | Listing 10-14 when we added a default implementation of the `summarize` method | |
177 | on the `Summary` trait. Any type implementing the `Summary` trait would have | |
178 | the `summarize` method available on it without any further code. This is | |
179 | similar to a parent class having an implementation of a method and an | |
180 | inheriting child class also having the implementation of the method. We can | |
181 | also override the default implementation of the `summarize` method when we | |
182 | implement the `Summary` trait, which is similar to a child class overriding the | |
5099ac24 FG |
183 | implementation of a method inherited from a parent class. |
184 | ||
923072b8 FG |
185 | <!-- I'm a bit uncomfortable with the above. I think it's more honest to say |
186 | that Rust doesn't support inheritance unless you use a macro. Saying to use | |
187 | the trait system to an OO programmer is going to leave them pretty confused, as | |
188 | traits lack of the basics of inheritance: you can't use and modify state, you | |
189 | have to use a surrogate type to hold the trait implementation, you can't | |
190 | instantiate, and so on. | |
191 | ||
192 | The example that came to mind: trying to teach OO programmers who want to | |
193 | build a UI library with traditional OO techniques using the trait system. | |
194 | It's unfortunately not going to work very well, if at all. | |
195 | ||
196 | A trait's main focus is polymorphism and not inheritance. It's probably | |
197 | better for folks coming from OO backgrounds if we just come out and say it, tbh. | |
198 | /JT --> | |
199 | <!-- I agree, and I've made some edits to the paragraphs above /Carol --> | |
200 | ||
5099ac24 FG |
201 | The other reason to use inheritance relates to the type system: to enable a |
202 | child type to be used in the same places as the parent type. This is also | |
203 | called *polymorphism*, which means that you can substitute multiple objects for | |
204 | each other at runtime if they share certain characteristics. | |
205 | ||
206 | > ### Polymorphism | |
207 | > | |
208 | > To many people, polymorphism is synonymous with inheritance. But it’s | |
209 | > actually a more general concept that refers to code that can work with data | |
210 | > of multiple types. For inheritance, those types are generally subclasses. | |
211 | > | |
212 | > Rust instead uses generics to abstract over different possible types and | |
213 | > trait bounds to impose constraints on what those types must provide. This is | |
214 | > sometimes called *bounded parametric polymorphism*. | |
215 | ||
216 | Inheritance has recently fallen out of favor as a programming design solution | |
217 | in many programming languages because it’s often at risk of sharing more code | |
218 | than necessary. Subclasses shouldn’t always share all characteristics of their | |
219 | parent class but will do so with inheritance. This can make a program’s design | |
220 | less flexible. It also introduces the possibility of calling methods on | |
221 | subclasses that don’t make sense or that cause errors because the methods don’t | |
923072b8 FG |
222 | apply to the subclass. In addition, some languages will only allow single |
223 | inheritance (meaning a subclass can only inherit from one class), further | |
224 | restricting the flexibility of a program’s design. | |
225 | ||
226 | <!-- Nit - "inherit from one class" and "single-inheritance" read a bit | |
227 | differently to me. Saying you inherit from only one class almost makes it sound | |
228 | like that the class you inherit from can't have a parent. Probably minor, just | |
229 | made me read that sentence a couple times. | |
230 | /JT --> | |
231 | <!-- I've included the term "single inheritance" above (it appears that usually | |
232 | it's not hyphenated) but kept what was there as an explanation in case the | |
233 | reader isn't familiar. /Carol --> | |
234 | ||
235 | For these reasons, Rust takes the different approach of using trait objects | |
236 | instead of inheritance. Let’s look at how trait objects enable polymorphism in | |
237 | Rust. | |
5099ac24 FG |
238 | |
239 | ## Using Trait Objects That Allow for Values of Different Types | |
240 | ||
241 | In Chapter 8, we mentioned that one limitation of vectors is that they can | |
242 | store elements of only one type. We created a workaround in Listing 8-10 where | |
243 | we defined a `SpreadsheetCell` enum that had variants to hold integers, floats, | |
244 | and text. This meant we could store different types of data in each cell and | |
245 | still have a vector that represented a row of cells. This is a perfectly good | |
246 | solution when our interchangeable items are a fixed set of types that we know | |
247 | when our code is compiled. | |
248 | ||
249 | However, sometimes we want our library user to be able to extend the set of | |
250 | types that are valid in a particular situation. To show how we might achieve | |
251 | this, we’ll create an example graphical user interface (GUI) tool that iterates | |
252 | through a list of items, calling a `draw` method on each one to draw it to the | |
253 | screen—a common technique for GUI tools. We’ll create a library crate called | |
254 | `gui` that contains the structure of a GUI library. This crate might include | |
255 | some types for people to use, such as `Button` or `TextField`. In addition, | |
256 | `gui` users will want to create their own types that can be drawn: for | |
257 | instance, one programmer might add an `Image` and another might add a | |
258 | `SelectBox`. | |
259 | ||
260 | We won’t implement a fully fledged GUI library for this example but will show | |
261 | how the pieces would fit together. At the time of writing the library, we can’t | |
262 | know and define all the types other programmers might want to create. But we do | |
263 | know that `gui` needs to keep track of many values of different types, and it | |
264 | needs to call a `draw` method on each of these differently typed values. It | |
265 | doesn’t need to know exactly what will happen when we call the `draw` method, | |
266 | just that the value will have that method available for us to call. | |
267 | ||
268 | To do this in a language with inheritance, we might define a class named | |
269 | `Component` that has a method named `draw` on it. The other classes, such as | |
270 | `Button`, `Image`, and `SelectBox`, would inherit from `Component` and thus | |
271 | inherit the `draw` method. They could each override the `draw` method to define | |
272 | their custom behavior, but the framework could treat all of the types as if | |
273 | they were `Component` instances and call `draw` on them. But because Rust | |
274 | doesn’t have inheritance, we need another way to structure the `gui` library to | |
275 | allow users to extend it with new types. | |
276 | ||
277 | ### Defining a Trait for Common Behavior | |
278 | ||
279 | To implement the behavior we want `gui` to have, we’ll define a trait named | |
280 | `Draw` that will have one method named `draw`. Then we can define a vector that | |
281 | takes a *trait object*. A trait object points to both an instance of a type | |
923072b8 FG |
282 | implementing our specified trait and a table used to look up trait methods on |
283 | that type at runtime. We create a trait object by specifying some sort of | |
284 | pointer, such as a `&` reference or a `Box<T>` smart pointer, then the `dyn` | |
285 | keyword, and then specifying the relevant trait. (We’ll talk about the reason | |
286 | trait objects must use a pointer in Chapter 19 in the section “Dynamically | |
287 | Sized Types and the `Sized` Trait.”) We can use trait objects in place of a | |
288 | generic or concrete type. Wherever we use a trait object, Rust’s type system | |
289 | will ensure at compile time that any value used in that context will implement | |
290 | the trait object’s trait. Consequently, we don’t need to know all the possible | |
291 | types at compile time. | |
292 | ||
293 | We’ve mentioned that, in Rust, we refrain from calling structs and enums | |
5099ac24 FG |
294 | “objects” to distinguish them from other languages’ objects. In a struct or |
295 | enum, the data in the struct fields and the behavior in `impl` blocks are | |
296 | separated, whereas in other languages, the data and behavior combined into one | |
297 | concept is often labeled an object. However, trait objects *are* more like | |
298 | objects in other languages in the sense that they combine data and behavior. | |
299 | But trait objects differ from traditional objects in that we can’t add data to | |
300 | a trait object. Trait objects aren’t as generally useful as objects in other | |
301 | languages: their specific purpose is to allow abstraction across common | |
302 | behavior. | |
303 | ||
304 | Listing 17-3 shows how to define a trait named `Draw` with one method named | |
305 | `draw`: | |
306 | ||
307 | Filename: src/lib.rs | |
308 | ||
309 | ``` | |
310 | pub trait Draw { | |
311 | fn draw(&self); | |
312 | } | |
313 | ``` | |
314 | ||
315 | Listing 17-3: Definition of the `Draw` trait | |
316 | ||
317 | This syntax should look familiar from our discussions on how to define traits | |
318 | in Chapter 10. Next comes some new syntax: Listing 17-4 defines a struct named | |
319 | `Screen` that holds a vector named `components`. This vector is of type | |
320 | `Box<dyn Draw>`, which is a trait object; it’s a stand-in for any type inside | |
321 | a `Box` that implements the `Draw` trait. | |
322 | ||
323 | Filename: src/lib.rs | |
324 | ||
325 | ``` | |
326 | pub struct Screen { | |
327 | pub components: Vec<Box<dyn Draw>>, | |
328 | } | |
329 | ``` | |
330 | ||
331 | Listing 17-4: Definition of the `Screen` struct with a `components` field | |
332 | holding a vector of trait objects that implement the `Draw` trait | |
333 | ||
334 | On the `Screen` struct, we’ll define a method named `run` that will call the | |
335 | `draw` method on each of its `components`, as shown in Listing 17-5: | |
336 | ||
337 | Filename: src/lib.rs | |
338 | ||
339 | ``` | |
340 | impl Screen { | |
341 | pub fn run(&self) { | |
342 | for component in self.components.iter() { | |
343 | component.draw(); | |
344 | } | |
345 | } | |
346 | } | |
347 | ``` | |
348 | ||
349 | Listing 17-5: A `run` method on `Screen` that calls the `draw` method on each | |
350 | component | |
351 | ||
352 | This works differently from defining a struct that uses a generic type | |
353 | parameter with trait bounds. A generic type parameter can only be substituted | |
354 | with one concrete type at a time, whereas trait objects allow for multiple | |
355 | concrete types to fill in for the trait object at runtime. For example, we | |
356 | could have defined the `Screen` struct using a generic type and a trait bound | |
357 | as in Listing 17-6: | |
358 | ||
359 | Filename: src/lib.rs | |
360 | ||
361 | ``` | |
362 | pub struct Screen<T: Draw> { | |
363 | pub components: Vec<T>, | |
364 | } | |
365 | ||
366 | impl<T> Screen<T> | |
367 | where | |
368 | T: Draw, | |
369 | { | |
370 | pub fn run(&self) { | |
371 | for component in self.components.iter() { | |
372 | component.draw(); | |
373 | } | |
374 | } | |
375 | } | |
376 | ``` | |
377 | ||
378 | Listing 17-6: An alternate implementation of the `Screen` struct and its `run` | |
379 | method using generics and trait bounds | |
380 | ||
381 | This restricts us to a `Screen` instance that has a list of components all of | |
382 | type `Button` or all of type `TextField`. If you’ll only ever have homogeneous | |
383 | collections, using generics and trait bounds is preferable because the | |
384 | definitions will be monomorphized at compile time to use the concrete types. | |
385 | ||
386 | On the other hand, with the method using trait objects, one `Screen` instance | |
387 | can hold a `Vec<T>` that contains a `Box<Button>` as well as a | |
388 | `Box<TextField>`. Let’s look at how this works, and then we’ll talk about the | |
389 | runtime performance implications. | |
390 | ||
391 | ### Implementing the Trait | |
392 | ||
393 | Now we’ll add some types that implement the `Draw` trait. We’ll provide the | |
394 | `Button` type. Again, actually implementing a GUI library is beyond the scope | |
395 | of this book, so the `draw` method won’t have any useful implementation in its | |
396 | body. To imagine what the implementation might look like, a `Button` struct | |
397 | might have fields for `width`, `height`, and `label`, as shown in Listing 17-7: | |
398 | ||
399 | Filename: src/lib.rs | |
400 | ||
401 | ``` | |
402 | pub struct Button { | |
403 | pub width: u32, | |
404 | pub height: u32, | |
405 | pub label: String, | |
406 | } | |
407 | ||
408 | impl Draw for Button { | |
409 | fn draw(&self) { | |
410 | // code to actually draw a button | |
411 | } | |
412 | } | |
413 | ``` | |
414 | ||
415 | Listing 17-7: A `Button` struct that implements the `Draw` trait | |
416 | ||
417 | The `width`, `height`, and `label` fields on `Button` will differ from the | |
923072b8 FG |
418 | fields on other components; for example, a `TextField` type might have those |
419 | same fields plus a `placeholder` field. Each of the types we want to draw on | |
5099ac24 FG |
420 | the screen will implement the `Draw` trait but will use different code in the |
421 | `draw` method to define how to draw that particular type, as `Button` has here | |
923072b8 FG |
422 | (without the actual GUI code, as mentioned). The `Button` type, for instance, |
423 | might have an additional `impl` block containing methods related to what | |
424 | happens when a user clicks the button. These kinds of methods won’t apply to | |
425 | types like `TextField`. | |
5099ac24 FG |
426 | |
427 | If someone using our library decides to implement a `SelectBox` struct that has | |
428 | `width`, `height`, and `options` fields, they implement the `Draw` trait on the | |
429 | `SelectBox` type as well, as shown in Listing 17-8: | |
430 | ||
431 | Filename: src/main.rs | |
432 | ||
433 | ``` | |
434 | use gui::Draw; | |
435 | ||
436 | struct SelectBox { | |
437 | width: u32, | |
438 | height: u32, | |
439 | options: Vec<String>, | |
440 | } | |
441 | ||
442 | impl Draw for SelectBox { | |
443 | fn draw(&self) { | |
444 | // code to actually draw a select box | |
445 | } | |
446 | } | |
447 | ``` | |
448 | ||
449 | Listing 17-8: Another crate using `gui` and implementing the `Draw` trait on a | |
450 | `SelectBox` struct | |
451 | ||
452 | Our library’s user can now write their `main` function to create a `Screen` | |
453 | instance. To the `Screen` instance, they can add a `SelectBox` and a `Button` | |
454 | by putting each in a `Box<T>` to become a trait object. They can then call the | |
455 | `run` method on the `Screen` instance, which will call `draw` on each of the | |
456 | components. Listing 17-9 shows this implementation: | |
457 | ||
458 | Filename: src/main.rs | |
459 | ||
460 | ``` | |
461 | use gui::{Button, Screen}; | |
462 | ||
463 | fn main() { | |
464 | let screen = Screen { | |
465 | components: vec![ | |
466 | Box::new(SelectBox { | |
467 | width: 75, | |
468 | height: 10, | |
469 | options: vec![ | |
470 | String::from("Yes"), | |
471 | String::from("Maybe"), | |
472 | String::from("No"), | |
473 | ], | |
474 | }), | |
475 | Box::new(Button { | |
476 | width: 50, | |
477 | height: 10, | |
478 | label: String::from("OK"), | |
479 | }), | |
480 | ], | |
481 | }; | |
482 | ||
483 | screen.run(); | |
484 | } | |
485 | ``` | |
486 | ||
923072b8 FG |
487 | <!-- I'd forgotten the UI components were in this chapter. To close on the |
488 | thought from earlier: we don't use any inheritance in our example, only | |
489 | polymorphism. This probably is a vote for my earlier suggestion. | |
490 | /JT --> | |
491 | <!-- I indeed took the earlier suggestion. /Carol --> | |
492 | ||
5099ac24 FG |
493 | Listing 17-9: Using trait objects to store values of different types that |
494 | implement the same trait | |
495 | ||
496 | When we wrote the library, we didn’t know that someone might add the | |
497 | `SelectBox` type, but our `Screen` implementation was able to operate on the | |
498 | new type and draw it because `SelectBox` implements the `Draw` trait, which | |
499 | means it implements the `draw` method. | |
500 | ||
501 | This concept—of being concerned only with the messages a value responds to | |
502 | rather than the value’s concrete type—is similar to the concept of *duck | |
503 | typing* in dynamically typed languages: if it walks like a duck and quacks | |
504 | like a duck, then it must be a duck! In the implementation of `run` on `Screen` | |
505 | in Listing 17-5, `run` doesn’t need to know what the concrete type of each | |
506 | component is. It doesn’t check whether a component is an instance of a `Button` | |
507 | or a `SelectBox`, it just calls the `draw` method on the component. By | |
508 | specifying `Box<dyn Draw>` as the type of the values in the `components` | |
509 | vector, we’ve defined `Screen` to need values that we can call the `draw` | |
510 | method on. | |
511 | ||
512 | The advantage of using trait objects and Rust’s type system to write code | |
513 | similar to code using duck typing is that we never have to check whether a | |
514 | value implements a particular method at runtime or worry about getting errors | |
515 | if a value doesn’t implement a method but we call it anyway. Rust won’t compile | |
516 | our code if the values don’t implement the traits that the trait objects need. | |
517 | ||
518 | For example, Listing 17-10 shows what happens if we try to create a `Screen` | |
519 | with a `String` as a component: | |
520 | ||
521 | Filename: src/main.rs | |
522 | ||
523 | ``` | |
524 | use gui::Screen; | |
525 | ||
526 | fn main() { | |
527 | let screen = Screen { | |
528 | components: vec![Box::new(String::from("Hi"))], | |
529 | }; | |
530 | ||
531 | screen.run(); | |
532 | } | |
533 | ``` | |
534 | ||
535 | Listing 17-10: Attempting to use a type that doesn’t | |
536 | implement the trait object’s trait | |
537 | ||
538 | We’ll get this error because `String` doesn’t implement the `Draw` trait: | |
539 | ||
540 | ``` | |
541 | error[E0277]: the trait bound `String: Draw` is not satisfied | |
542 | --> src/main.rs:5:26 | |
543 | | | |
544 | 5 | components: vec![Box::new(String::from("Hi"))], | |
545 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String` | |
546 | | | |
547 | = note: required for the cast to the object type `dyn Draw` | |
548 | ``` | |
549 | ||
550 | This error lets us know that either we’re passing something to `Screen` we | |
923072b8 | 551 | didn’t mean to pass and so should pass a different type or we should implement |
5099ac24 FG |
552 | `Draw` on `String` so that `Screen` is able to call `draw` on it. |
553 | ||
554 | ### Trait Objects Perform Dynamic Dispatch | |
555 | ||
556 | Recall in the “Performance of Code Using Generics” section in Chapter 10 our | |
557 | discussion on the monomorphization process performed by the compiler when we | |
558 | use trait bounds on generics: the compiler generates nongeneric implementations | |
559 | of functions and methods for each concrete type that we use in place of a | |
560 | generic type parameter. The code that results from monomorphization is doing | |
561 | *static dispatch*, which is when the compiler knows what method you’re calling | |
562 | at compile time. This is opposed to *dynamic dispatch*, which is when the | |
563 | compiler can’t tell at compile time which method you’re calling. In dynamic | |
564 | dispatch cases, the compiler emits code that at runtime will figure out which | |
565 | method to call. | |
566 | ||
567 | When we use trait objects, Rust must use dynamic dispatch. The compiler doesn’t | |
923072b8 FG |
568 | know all the types that might be used with the code that’s using trait objects, |
569 | so it doesn’t know which method implemented on which type to call. Instead, at | |
570 | runtime, Rust uses the pointers inside the trait object to know which method to | |
571 | call. This lookup incurs a runtime cost that doesn’t occur with static | |
572 | dispatch. Dynamic dispatch also prevents the compiler from choosing to inline a | |
573 | method’s code, which in turn prevents some optimizations. However, we did get | |
574 | extra flexibility in the code that we wrote in Listing 17-5 and were able to | |
575 | support in Listing 17-9, so it’s a trade-off to consider. | |
5099ac24 FG |
576 | |
577 | ## Implementing an Object-Oriented Design Pattern | |
578 | ||
579 | The *state pattern* is an object-oriented design pattern. The crux of the | |
923072b8 FG |
580 | pattern is that we define a set of states a value can have internally. The |
581 | states are represented by a set of *state objects*, and the value’s behavior | |
582 | changes based on its state. We’re going to work through an example of a blog | |
583 | post struct that has a field to hold its state, which will be a state object | |
584 | from the set "draft", "review", or "published". | |
585 | <!-- can you give a quick example here, something we could visualize? are we | |
586 | saying "we define a set of states a value can have as state objects...."? /LC | |
587 | --> | |
588 | <!-- What do you think about this, hinting at the coming example quickly? It | |
589 | felt weird to introduce something different only to switch gears in a few | |
590 | paragraphs, so is moving the example's introduction here ok? /Carol --> | |
591 | <!-- JT, what do you think? /LC --> | |
592 | <!-- Seems okay. My one thought coming to the end of the paragraph was "is | |
593 | this better than using an enum?" Not sure if we want to sidebar a bit on | |
594 | why we chose traits over enums, but some readers might be curious. | |
595 | /JT --> | |
596 | <!-- I've added a box later titled "Why Not An Enum?" to address this -- I | |
597 | think that makes a nice exercise for the reader :) /Carol --> | |
598 | ||
599 | The state objects share functionality: in Rust, of course, we use | |
5099ac24 FG |
600 | structs and traits rather than objects and inheritance. Each state object is |
601 | responsible for its own behavior and for governing when it should change into | |
602 | another state. The value that holds a state object knows nothing about the | |
603 | different behavior of the states or when to transition between states. | |
604 | ||
923072b8 FG |
605 | The advantage of using the state pattern is that, when the business |
606 | requirements of the program change, we won’t need to change the code of the | |
607 | value holding the state or the code that uses the value. We’ll only need to | |
608 | update the code inside one of the state objects to change its rules or perhaps | |
609 | add more state objects. | |
610 | ||
611 | First, we’re going to implement the state pattern in a more traditional | |
612 | object-oriented way, then we’ll use an approach that’s a bit more natural in | |
613 | Rust. Let’s dig in to incrementally implementing a blog post workflow using the | |
614 | state pattern. | |
5099ac24 | 615 | |
923072b8 | 616 | The final functionality will look like this: |
5099ac24 FG |
617 | |
618 | 1. A blog post starts as an empty draft. | |
619 | 2. When the draft is done, a review of the post is requested. | |
620 | 3. When the post is approved, it gets published. | |
621 | 4. Only published blog posts return content to print, so unapproved posts can’t | |
622 | accidentally be published. | |
623 | ||
624 | Any other changes attempted on a post should have no effect. For example, if we | |
625 | try to approve a draft blog post before we’ve requested a review, the post | |
626 | should remain an unpublished draft. | |
627 | ||
628 | Listing 17-11 shows this workflow in code form: this is an example usage of the | |
629 | API we’ll implement in a library crate named `blog`. This won’t compile yet | |
923072b8 | 630 | because we haven’t implemented the `blog` crate. |
5099ac24 FG |
631 | |
632 | Filename: src/main.rs | |
633 | ||
634 | ``` | |
635 | use blog::Post; | |
636 | ||
637 | fn main() { | |
638 | [1] let mut post = Post::new(); | |
639 | ||
640 | [2] post.add_text("I ate a salad for lunch today"); | |
641 | [3] assert_eq!("", post.content()); | |
642 | ||
643 | [4] post.request_review(); | |
644 | [5] assert_eq!("", post.content()); | |
645 | ||
646 | [6] post.approve(); | |
647 | [7] assert_eq!("I ate a salad for lunch today", post.content()); | |
648 | } | |
649 | ``` | |
650 | ||
651 | Listing 17-11: Code that demonstrates the desired behavior we want our `blog` | |
652 | crate to have | |
653 | ||
654 | We want to allow the user to create a new draft blog post with `Post::new` [1]. | |
655 | We want to allow text to be added to the blog post [2]. If we try to get the | |
923072b8 | 656 | post’s content immediately, before approval, we shouldn’t get any text because |
5099ac24 FG |
657 | the post is still a draft. We’ve added `assert_eq!` in the code for |
658 | demonstration purposes [3]. An excellent unit test for this would be to assert | |
659 | that a draft blog post returns an empty string from the `content` method, but | |
660 | we’re not going to write tests for this example. | |
661 | ||
662 | Next, we want to enable a request for a review of the post [4], and we want | |
663 | `content` to return an empty string while waiting for the review [5]. When the | |
664 | post receives approval [6], it should get published, meaning the text of the | |
665 | post will be returned when `content` is called [7]. | |
666 | ||
667 | Notice that the only type we’re interacting with from the crate is the `Post` | |
668 | type. This type will use the state pattern and will hold a value that will be | |
669 | one of three state objects representing the various states a post can be | |
670 | in—draft, waiting for review, or published. Changing from one state to another | |
671 | will be managed internally within the `Post` type. The states change in | |
672 | response to the methods called by our library’s users on the `Post` instance, | |
673 | but they don’t have to manage the state changes directly. Also, users can’t | |
674 | make a mistake with the states, like publishing a post before it’s reviewed. | |
675 | ||
676 | ### Defining `Post` and Creating a New Instance in the Draft State | |
677 | ||
678 | Let’s get started on the implementation of the library! We know we need a | |
679 | public `Post` struct that holds some content, so we’ll start with the | |
680 | definition of the struct and an associated public `new` function to create an | |
681 | instance of `Post`, as shown in Listing 17-12. We’ll also make a private | |
923072b8 FG |
682 | `State` trait that will define the behavior that all state objects for a `Post` |
683 | must have. | |
684 | <!-- JT, I had a few questions here about what the state objects and state | |
685 | traits are doing. I'd appreciate your view on whether this all reads well with | |
686 | nothing missing! /LC --> | |
687 | <!-- Seems okay. If you're going to try to use a traditional OO approach in | |
688 | Rust, it'll have a bit of this style. I'm glad we include something that's a | |
689 | bit more Rust-y at the end of the chapter. | |
690 | ||
691 | What I might suggest is that we give the reader a bit of a roadmap here to say | |
692 | that we're going to explore two solutions to this problem. The first, a more | |
693 | traditional approach encoded into Rust, and the second, an approach that's more | |
694 | natural to Rust. | |
695 | /JT --> | |
696 | <!-- Great idea! I've added a bit in the introduction of this section above -- | |
697 | "First, we’re going to implement the state pattern in a more traditional | |
698 | object-oriented way, then we’ll use an approach that’s a bit more natural in | |
699 | Rust." /Carol --> | |
700 | ||
701 | Then `Post` will hold a trait object of `Box<dyn State>` inside an `Option<T>` | |
702 | in a private field named `state` to hold the state object. You’ll see why the | |
5099ac24 FG |
703 | `Option<T>` is necessary in a bit. |
704 | ||
705 | Filename: src/lib.rs | |
706 | ||
707 | ``` | |
708 | pub struct Post { | |
709 | state: Option<Box<dyn State>>, | |
710 | content: String, | |
711 | } | |
712 | ||
713 | impl Post { | |
714 | pub fn new() -> Post { | |
715 | Post { | |
716 | [1] state: Some(Box::new(Draft {})), | |
717 | [2] content: String::new(), | |
718 | } | |
719 | } | |
720 | } | |
721 | ||
722 | trait State {} | |
723 | ||
724 | struct Draft {} | |
725 | ||
726 | impl State for Draft {} | |
727 | ``` | |
728 | ||
729 | Listing 17-12: Definition of a `Post` struct and a `new` function that creates | |
730 | a new `Post` instance, a `State` trait, and a `Draft` struct | |
731 | ||
923072b8 FG |
732 | The `State` trait defines the behavior shared by different post states. The |
733 | state objects are `Draft`, `PendingReview`, and `Published`, and they will all | |
734 | implement the `State` trait. For now, the trait doesn’t have any methods, and | |
735 | we’ll start by defining just the `Draft` state because that is the state we | |
736 | want a post to start in. | |
5099ac24 FG |
737 | |
738 | When we create a new `Post`, we set its `state` field to a `Some` value that | |
739 | holds a `Box` [1]. This `Box` points to a new instance of the `Draft` struct. | |
740 | This ensures whenever we create a new instance of `Post`, it will start out as | |
741 | a draft. Because the `state` field of `Post` is private, there is no way to | |
742 | create a `Post` in any other state! In the `Post::new` function, we set the | |
743 | `content` field to a new, empty `String` [2]. | |
744 | ||
745 | ### Storing the Text of the Post Content | |
746 | ||
923072b8 FG |
747 | We saw in Listing 17-11 that we want to be able to call a method named |
748 | `add_text` and pass it a `&str` that is then added as the text content of the | |
749 | blog post. We implement this as a method, rather than exposing the `content` | |
750 | field as `pub`, so that later we can implement a method that will control how | |
751 | the `content` field’s data is read. The `add_text` method is pretty | |
5099ac24 FG |
752 | straightforward, so let’s add the implementation in Listing 17-13 to the `impl |
753 | Post` block: | |
754 | ||
755 | Filename: src/lib.rs | |
756 | ||
757 | ``` | |
758 | impl Post { | |
759 | // --snip-- | |
760 | pub fn add_text(&mut self, text: &str) { | |
761 | self.content.push_str(text); | |
762 | } | |
763 | } | |
764 | ``` | |
765 | ||
766 | Listing 17-13: Implementing the `add_text` method to add text to a post’s | |
767 | `content` | |
768 | ||
769 | The `add_text` method takes a mutable reference to `self`, because we’re | |
770 | changing the `Post` instance that we’re calling `add_text` on. We then call | |
771 | `push_str` on the `String` in `content` and pass the `text` argument to add to | |
772 | the saved `content`. This behavior doesn’t depend on the state the post is in, | |
773 | so it’s not part of the state pattern. The `add_text` method doesn’t interact | |
774 | with the `state` field at all, but it is part of the behavior we want to | |
775 | support. | |
776 | ||
777 | ### Ensuring the Content of a Draft Post Is Empty | |
778 | ||
779 | Even after we’ve called `add_text` and added some content to our post, we still | |
780 | want the `content` method to return an empty string slice because the post is | |
781 | still in the draft state, as shown at [3] in Listing 17-11. For now, let’s | |
782 | implement the `content` method with the simplest thing that will fulfill this | |
783 | requirement: always returning an empty string slice. We’ll change this later | |
784 | once we implement the ability to change a post’s state so it can be published. | |
785 | So far, posts can only be in the draft state, so the post content should always | |
786 | be empty. Listing 17-14 shows this placeholder implementation: | |
787 | ||
788 | Filename: src/lib.rs | |
789 | ||
790 | ``` | |
791 | impl Post { | |
792 | // --snip-- | |
793 | pub fn content(&self) -> &str { | |
794 | "" | |
795 | } | |
796 | } | |
797 | ``` | |
798 | ||
799 | Listing 17-14: Adding a placeholder implementation for the `content` method on | |
800 | `Post` that always returns an empty string slice | |
801 | ||
802 | With this added `content` method, everything in Listing 17-11 up to the line at | |
803 | [3] works as intended. | |
804 | ||
805 | ### Requesting a Review of the Post Changes Its State | |
806 | ||
807 | Next, we need to add functionality to request a review of a post, which should | |
808 | change its state from `Draft` to `PendingReview`. Listing 17-15 shows this code: | |
809 | ||
810 | Filename: src/lib.rs | |
811 | ||
812 | ``` | |
813 | impl Post { | |
814 | // --snip-- | |
815 | [1] pub fn request_review(&mut self) { | |
816 | [2] if let Some(s) = self.state.take() { | |
817 | [3] self.state = Some(s.request_review()) | |
818 | } | |
819 | } | |
820 | } | |
821 | ||
822 | trait State { | |
823 | [4] fn request_review(self: Box<Self>) -> Box<dyn State>; | |
824 | } | |
825 | ||
826 | struct Draft {} | |
827 | ||
828 | impl State for Draft { | |
829 | fn request_review(self: Box<Self>) -> Box<dyn State> { | |
830 | [5] Box::new(PendingReview {}) | |
831 | } | |
832 | } | |
833 | ||
834 | struct PendingReview {} | |
835 | ||
836 | impl State for PendingReview { | |
837 | fn request_review(self: Box<Self>) -> Box<dyn State> { | |
838 | [6] self | |
839 | } | |
840 | } | |
841 | ``` | |
842 | ||
843 | Listing 17-15: Implementing `request_review` methods on `Post` and the `State` | |
844 | trait | |
845 | ||
846 | We give `Post` a public method named `request_review` that will take a mutable | |
847 | reference to `self` [1]. Then we call an internal `request_review` method on | |
848 | the current state of `Post` [3], and this second `request_review` method | |
849 | consumes the current state and returns a new state. | |
850 | ||
923072b8 FG |
851 | We add the `request_review` method to the `State` trait [4]; all types that |
852 | implement the trait will now need to implement the `request_review` method. | |
853 | Note that rather than having `self`, `&self`, or `&mut self` as the first | |
854 | parameter of the method, we have `self: Box<Self>`. This syntax means the | |
5099ac24 FG |
855 | method is only valid when called on a `Box` holding the type. This syntax takes |
856 | ownership of `Box<Self>`, invalidating the old state so the state value of the | |
857 | `Post` can transform into a new state. | |
858 | ||
859 | To consume the old state, the `request_review` method needs to take ownership | |
860 | of the state value. This is where the `Option` in the `state` field of `Post` | |
861 | comes in: we call the `take` method to take the `Some` value out of the `state` | |
862 | field and leave a `None` in its place, because Rust doesn’t let us have | |
863 | unpopulated fields in structs [2]. This lets us move the `state` value out of | |
864 | `Post` rather than borrowing it. Then we’ll set the post’s `state` value to the | |
865 | result of this operation. | |
866 | ||
867 | We need to set `state` to `None` temporarily rather than setting it directly | |
868 | with code like `self.state = self.state.request_review();` to get ownership of | |
869 | the `state` value. This ensures `Post` can’t use the old `state` value after | |
870 | we’ve transformed it into a new state. | |
871 | ||
923072b8 FG |
872 | The `request_review` method on `Draft` returns a new, boxed instance of a new |
873 | `PendingReview` struct [5], which represents the state when a post is waiting | |
874 | for a review. The `PendingReview` struct also implements the `request_review` | |
875 | method but doesn’t do any transformations. Rather, it returns itself [6], | |
876 | because when we request a review on a post already in the `PendingReview` | |
877 | state, it should stay in the `PendingReview` state. | |
5099ac24 FG |
878 | |
879 | Now we can start seeing the advantages of the state pattern: the | |
880 | `request_review` method on `Post` is the same no matter its `state` value. Each | |
881 | state is responsible for its own rules. | |
882 | ||
883 | We’ll leave the `content` method on `Post` as is, returning an empty string | |
884 | slice. We can now have a `Post` in the `PendingReview` state as well as in the | |
885 | `Draft` state, but we want the same behavior in the `PendingReview` state. | |
886 | Listing 17-11 now works up to the line at [5]! | |
887 | ||
923072b8 | 888 | ### Adding `approve` to Change the Behavior of `content` |
5099ac24 FG |
889 | |
890 | The `approve` method will be similar to the `request_review` method: it will | |
891 | set `state` to the value that the current state says it should have when that | |
892 | state is approved, as shown in Listing 17-16: | |
893 | ||
894 | Filename: src/lib.rs | |
895 | ||
896 | ``` | |
897 | impl Post { | |
898 | // --snip-- | |
899 | pub fn approve(&mut self) { | |
900 | if let Some(s) = self.state.take() { | |
901 | self.state = Some(s.approve()) | |
902 | } | |
903 | } | |
904 | } | |
905 | ||
906 | trait State { | |
907 | fn request_review(self: Box<Self>) -> Box<dyn State>; | |
908 | fn approve(self: Box<Self>) -> Box<dyn State>; | |
909 | } | |
910 | ||
911 | struct Draft {} | |
912 | ||
913 | impl State for Draft { | |
914 | // --snip-- | |
915 | fn approve(self: Box<Self>) -> Box<dyn State> { | |
916 | [1] self | |
917 | } | |
918 | } | |
919 | ||
920 | struct PendingReview {} | |
921 | ||
922 | impl State for PendingReview { | |
923 | // --snip-- | |
924 | fn approve(self: Box<Self>) -> Box<dyn State> { | |
925 | [2] Box::new(Published {}) | |
926 | } | |
927 | } | |
928 | ||
929 | struct Published {} | |
930 | ||
931 | impl State for Published { | |
932 | fn request_review(self: Box<Self>) -> Box<dyn State> { | |
933 | self | |
934 | } | |
935 | ||
936 | fn approve(self: Box<Self>) -> Box<dyn State> { | |
937 | self | |
938 | } | |
939 | } | |
940 | ``` | |
941 | ||
942 | Listing 17-16: Implementing the `approve` method on `Post` and the `State` trait | |
943 | ||
944 | We add the `approve` method to the `State` trait and add a new struct that | |
945 | implements `State`, the `Published` state. | |
946 | ||
947 | Similar to the way `request_review` on `PendingReview` works, if we call the | |
948 | `approve` method on a `Draft`, it will have no effect because `approve` will | |
949 | return `self` [1]. When we call `approve` on `PendingReview`, it returns a new, | |
950 | boxed instance of the `Published` struct [2]. The `Published` struct implements | |
951 | the `State` trait, and for both the `request_review` method and the `approve` | |
952 | method, it returns itself, because the post should stay in the `Published` | |
953 | state in those cases. | |
954 | ||
955 | Now we need to update the `content` method on `Post`. We want the value | |
923072b8 | 956 | returned from `content` to depend on the current state of the `Post`, so we’re |
5099ac24 FG |
957 | going to have the `Post` delegate to a `content` method defined on its `state`, |
958 | as shown in Listing 17-17: | |
959 | ||
960 | Filename: src/lib.rs | |
961 | ||
962 | ``` | |
963 | impl Post { | |
964 | // --snip-- | |
965 | pub fn content(&self) -> &str { | |
966 | self.state.as_ref().unwrap().content(self) | |
967 | } | |
968 | // --snip-- | |
969 | } | |
970 | ``` | |
971 | ||
972 | Listing 17-17: Updating the `content` method on `Post` to delegate to a | |
973 | `content` method on `State` | |
974 | ||
975 | Because the goal is to keep all these rules inside the structs that implement | |
976 | `State`, we call a `content` method on the value in `state` and pass the post | |
923072b8 | 977 | instance (that is, `self`) as an argument. Then we return the value that’s |
5099ac24 FG |
978 | returned from using the `content` method on the `state` value. |
979 | ||
980 | We call the `as_ref` method on the `Option` because we want a reference to the | |
981 | value inside the `Option` rather than ownership of the value. Because `state` | |
982 | is an `Option<Box<dyn State>>`, when we call `as_ref`, an `Option<&Box<dyn | |
983 | State>>` is returned. If we didn’t call `as_ref`, we would get an error because | |
984 | we can’t move `state` out of the borrowed `&self` of the function parameter. | |
985 | ||
986 | We then call the `unwrap` method, which we know will never panic, because we | |
987 | know the methods on `Post` ensure that `state` will always contain a `Some` | |
988 | value when those methods are done. This is one of the cases we talked about in | |
923072b8 FG |
989 | the “Cases In Which You Have More Information Than the Compiler” section of |
990 | Chapter 9 when we know that a `None` value is never possible, even though the | |
991 | compiler isn’t able to understand that. | |
5099ac24 FG |
992 | |
993 | At this point, when we call `content` on the `&Box<dyn State>`, deref coercion | |
994 | will take effect on the `&` and the `Box` so the `content` method will | |
995 | ultimately be called on the type that implements the `State` trait. That means | |
996 | we need to add `content` to the `State` trait definition, and that is where | |
997 | we’ll put the logic for what content to return depending on which state we | |
998 | have, as shown in Listing 17-18: | |
999 | ||
1000 | Filename: src/lib.rs | |
1001 | ||
1002 | ``` | |
1003 | trait State { | |
1004 | // --snip-- | |
1005 | fn content<'a>(&self, post: &'a Post) -> &'a str { | |
1006 | [1] "" | |
1007 | } | |
1008 | } | |
1009 | ||
1010 | // --snip-- | |
1011 | struct Published {} | |
1012 | ||
1013 | impl State for Published { | |
1014 | // --snip-- | |
1015 | fn content<'a>(&self, post: &'a Post) -> &'a str { | |
1016 | [2] &post.content | |
1017 | } | |
1018 | } | |
1019 | ``` | |
1020 | ||
1021 | Listing 17-18: Adding the `content` method to the `State` trait | |
1022 | ||
1023 | We add a default implementation for the `content` method that returns an empty | |
1024 | string slice [1]. That means we don’t need to implement `content` on the `Draft` | |
1025 | and `PendingReview` structs. The `Published` struct will override the `content` | |
1026 | method and return the value in `post.content` [2]. | |
1027 | ||
1028 | Note that we need lifetime annotations on this method, as we discussed in | |
1029 | Chapter 10. We’re taking a reference to a `post` as an argument and returning a | |
1030 | reference to part of that `post`, so the lifetime of the returned reference is | |
1031 | related to the lifetime of the `post` argument. | |
1032 | ||
1033 | And we’re done—all of Listing 17-11 now works! We’ve implemented the state | |
1034 | pattern with the rules of the blog post workflow. The logic related to the | |
1035 | rules lives in the state objects rather than being scattered throughout `Post`. | |
1036 | ||
923072b8 FG |
1037 | > #### Why Not An Enum? |
1038 | > | |
1039 | > You may have been wondering why we didn’t use an `enum` with the different | |
1040 | > possible post states as variants. That’s certainly a possible solution, try | |
1041 | > it and compare the end results to see which you prefer! One disadvantage of | |
1042 | > using an enum is every place that checks the value of the enum will need a | |
1043 | > `match` expression or similar to handle every possible variant. This could | |
1044 | > get more repetitive than this trait object solution. | |
1045 | ||
5099ac24 FG |
1046 | ### Trade-offs of the State Pattern |
1047 | ||
1048 | We’ve shown that Rust is capable of implementing the object-oriented state | |
1049 | pattern to encapsulate the different kinds of behavior a post should have in | |
1050 | each state. The methods on `Post` know nothing about the various behaviors. The | |
1051 | way we organized the code, we have to look in only one place to know the | |
1052 | different ways a published post can behave: the implementation of the `State` | |
1053 | trait on the `Published` struct. | |
1054 | ||
1055 | If we were to create an alternative implementation that didn’t use the state | |
1056 | pattern, we might instead use `match` expressions in the methods on `Post` or | |
1057 | even in the `main` code that checks the state of the post and changes behavior | |
1058 | in those places. That would mean we would have to look in several places to | |
1059 | understand all the implications of a post being in the published state! This | |
1060 | would only increase the more states we added: each of those `match` expressions | |
1061 | would need another arm. | |
1062 | ||
1063 | With the state pattern, the `Post` methods and the places we use `Post` don’t | |
1064 | need `match` expressions, and to add a new state, we would only need to add a | |
1065 | new struct and implement the trait methods on that one struct. | |
1066 | ||
1067 | The implementation using the state pattern is easy to extend to add more | |
1068 | functionality. To see the simplicity of maintaining code that uses the state | |
1069 | pattern, try a few of these suggestions: | |
1070 | ||
1071 | * Add a `reject` method that changes the post’s state from `PendingReview` back | |
1072 | to `Draft`. | |
1073 | * Require two calls to `approve` before the state can be changed to `Published`. | |
1074 | * Allow users to add text content only when a post is in the `Draft` state. | |
1075 | Hint: have the state object responsible for what might change about the | |
1076 | content but not responsible for modifying the `Post`. | |
1077 | ||
1078 | One downside of the state pattern is that, because the states implement the | |
1079 | transitions between states, some of the states are coupled to each other. If we | |
1080 | add another state between `PendingReview` and `Published`, such as `Scheduled`, | |
1081 | we would have to change the code in `PendingReview` to transition to | |
1082 | `Scheduled` instead. It would be less work if `PendingReview` didn’t need to | |
1083 | change with the addition of a new state, but that would mean switching to | |
1084 | another design pattern. | |
1085 | ||
1086 | Another downside is that we’ve duplicated some logic. To eliminate some of the | |
1087 | duplication, we might try to make default implementations for the | |
1088 | `request_review` and `approve` methods on the `State` trait that return `self`; | |
1089 | however, this would violate object safety, because the trait doesn’t know what | |
1090 | the concrete `self` will be exactly. We want to be able to use `State` as a | |
1091 | trait object, so we need its methods to be object safe. | |
1092 | ||
1093 | Other duplication includes the similar implementations of the `request_review` | |
1094 | and `approve` methods on `Post`. Both methods delegate to the implementation of | |
1095 | the same method on the value in the `state` field of `Option` and set the new | |
1096 | value of the `state` field to the result. If we had a lot of methods on `Post` | |
1097 | that followed this pattern, we might consider defining a macro to eliminate the | |
1098 | repetition (see the “Macros” section in Chapter 19). | |
1099 | ||
1100 | By implementing the state pattern exactly as it’s defined for object-oriented | |
1101 | languages, we’re not taking as full advantage of Rust’s strengths as we could. | |
1102 | Let’s look at some changes we can make to the `blog` crate that can make | |
1103 | invalid states and transitions into compile time errors. | |
1104 | ||
1105 | #### Encoding States and Behavior as Types | |
1106 | ||
1107 | We’ll show you how to rethink the state pattern to get a different set of | |
1108 | trade-offs. Rather than encapsulating the states and transitions completely so | |
1109 | outside code has no knowledge of them, we’ll encode the states into different | |
1110 | types. Consequently, Rust’s type checking system will prevent attempts to use | |
1111 | draft posts where only published posts are allowed by issuing a compiler error. | |
1112 | ||
1113 | Let’s consider the first part of `main` in Listing 17-11: | |
1114 | ||
1115 | Filename: src/main.rs | |
1116 | ||
1117 | ``` | |
1118 | fn main() { | |
1119 | let mut post = Post::new(); | |
1120 | ||
1121 | post.add_text("I ate a salad for lunch today"); | |
1122 | assert_eq!("", post.content()); | |
1123 | } | |
1124 | ``` | |
1125 | ||
1126 | We still enable the creation of new posts in the draft state using `Post::new` | |
1127 | and the ability to add text to the post’s content. But instead of having a | |
1128 | `content` method on a draft post that returns an empty string, we’ll make it so | |
1129 | draft posts don’t have the `content` method at all. That way, if we try to get | |
1130 | a draft post’s content, we’ll get a compiler error telling us the method | |
1131 | doesn’t exist. As a result, it will be impossible for us to accidentally | |
1132 | display draft post content in production, because that code won’t even compile. | |
1133 | Listing 17-19 shows the definition of a `Post` struct and a `DraftPost` struct, | |
1134 | as well as methods on each: | |
1135 | ||
1136 | Filename: src/lib.rs | |
1137 | ||
1138 | ``` | |
1139 | pub struct Post { | |
1140 | content: String, | |
1141 | } | |
1142 | ||
1143 | pub struct DraftPost { | |
1144 | content: String, | |
1145 | } | |
1146 | ||
1147 | impl Post { | |
1148 | [1] pub fn new() -> DraftPost { | |
1149 | DraftPost { | |
1150 | content: String::new(), | |
1151 | } | |
1152 | } | |
1153 | ||
1154 | [2] pub fn content(&self) -> &str { | |
1155 | &self.content | |
1156 | } | |
1157 | } | |
1158 | ||
1159 | impl DraftPost { | |
1160 | [3] pub fn add_text(&mut self, text: &str) { | |
1161 | self.content.push_str(text); | |
1162 | } | |
1163 | } | |
1164 | ``` | |
1165 | ||
1166 | Listing 17-19: A `Post` with a `content` method and a `DraftPost` without a | |
1167 | `content` method | |
1168 | ||
1169 | Both the `Post` and `DraftPost` structs have a private `content` field that | |
1170 | stores the blog post text. The structs no longer have the `state` field because | |
1171 | we’re moving the encoding of the state to the types of the structs. The `Post` | |
1172 | struct will represent a published post, and it has a `content` method that | |
1173 | returns the `content` [2]. | |
1174 | ||
1175 | We still have a `Post::new` function, but instead of returning an instance of | |
1176 | `Post`, it returns an instance of `DraftPost` [1]. Because `content` is private | |
1177 | and there aren’t any functions that return `Post`, it’s not possible to create | |
1178 | an instance of `Post` right now. | |
1179 | ||
1180 | The `DraftPost` struct has an `add_text` method, so we can add text to | |
1181 | `content` as before [3], but note that `DraftPost` does not have a `content` | |
1182 | method defined! So now the program ensures all posts start as draft posts, and | |
1183 | draft posts don’t have their content available for display. Any attempt to get | |
1184 | around these constraints will result in a compiler error. | |
1185 | ||
1186 | #### Implementing Transitions as Transformations into Different Types | |
1187 | ||
1188 | So how do we get a published post? We want to enforce the rule that a draft | |
1189 | post has to be reviewed and approved before it can be published. A post in the | |
1190 | pending review state should still not display any content. Let’s implement | |
1191 | these constraints by adding another struct, `PendingReviewPost`, defining the | |
1192 | `request_review` method on `DraftPost` to return a `PendingReviewPost`, and | |
1193 | defining an `approve` method on `PendingReviewPost` to return a `Post`, as | |
1194 | shown in Listing 17-20: | |
1195 | ||
1196 | Filename: src/lib.rs | |
1197 | ||
1198 | ``` | |
1199 | impl DraftPost { | |
1200 | // --snip-- | |
1201 | pub fn request_review(self) -> PendingReviewPost { | |
1202 | PendingReviewPost { | |
1203 | content: self.content, | |
1204 | } | |
1205 | } | |
1206 | } | |
1207 | ||
1208 | pub struct PendingReviewPost { | |
1209 | content: String, | |
1210 | } | |
1211 | ||
1212 | impl PendingReviewPost { | |
1213 | pub fn approve(self) -> Post { | |
1214 | Post { | |
1215 | content: self.content, | |
1216 | } | |
1217 | } | |
1218 | } | |
1219 | ``` | |
1220 | ||
1221 | Listing 17-20: A `PendingReviewPost` that gets created by calling | |
1222 | `request_review` on `DraftPost` and an `approve` method that turns a | |
1223 | `PendingReviewPost` into a published `Post` | |
1224 | ||
1225 | The `request_review` and `approve` methods take ownership of `self`, thus | |
1226 | consuming the `DraftPost` and `PendingReviewPost` instances and transforming | |
1227 | them into a `PendingReviewPost` and a published `Post`, respectively. This way, | |
1228 | we won’t have any lingering `DraftPost` instances after we’ve called | |
1229 | `request_review` on them, and so forth. The `PendingReviewPost` struct doesn’t | |
1230 | have a `content` method defined on it, so attempting to read its content | |
1231 | results in a compiler error, as with `DraftPost`. Because the only way to get a | |
1232 | published `Post` instance that does have a `content` method defined is to call | |
1233 | the `approve` method on a `PendingReviewPost`, and the only way to get a | |
1234 | `PendingReviewPost` is to call the `request_review` method on a `DraftPost`, | |
1235 | we’ve now encoded the blog post workflow into the type system. | |
1236 | ||
1237 | But we also have to make some small changes to `main`. The `request_review` and | |
1238 | `approve` methods return new instances rather than modifying the struct they’re | |
1239 | called on, so we need to add more `let post =` shadowing assignments to save | |
1240 | the returned instances. We also can’t have the assertions about the draft and | |
1241 | pending review posts’ contents be empty strings, nor do we need them: we can’t | |
1242 | compile code that tries to use the content of posts in those states any longer. | |
1243 | The updated code in `main` is shown in Listing 17-21: | |
1244 | ||
1245 | Filename: src/main.rs | |
1246 | ||
1247 | ``` | |
1248 | use blog::Post; | |
1249 | ||
1250 | fn main() { | |
1251 | let mut post = Post::new(); | |
1252 | ||
1253 | post.add_text("I ate a salad for lunch today"); | |
1254 | ||
1255 | let post = post.request_review(); | |
1256 | ||
1257 | let post = post.approve(); | |
1258 | ||
1259 | assert_eq!("I ate a salad for lunch today", post.content()); | |
1260 | } | |
1261 | ``` | |
1262 | ||
1263 | Listing 17-21: Modifications to `main` to use the new implementation of the | |
1264 | blog post workflow | |
1265 | ||
1266 | The changes we needed to make to `main` to reassign `post` mean that this | |
1267 | implementation doesn’t quite follow the object-oriented state pattern anymore: | |
1268 | the transformations between the states are no longer encapsulated entirely | |
1269 | within the `Post` implementation. However, our gain is that invalid states are | |
1270 | now impossible because of the type system and the type checking that happens at | |
1271 | compile time! This ensures that certain bugs, such as display of the content of | |
1272 | an unpublished post, will be discovered before they make it to production. | |
1273 | ||
923072b8 FG |
1274 | Try the tasks suggested at the start of this section on the `blog` crate as it |
1275 | is after Listing 17-21 to see what you think about the design of this version | |
1276 | of the code. Note that some of the tasks might be completed already in this | |
1277 | design. | |
5099ac24 FG |
1278 | |
1279 | We’ve seen that even though Rust is capable of implementing object-oriented | |
1280 | design patterns, other patterns, such as encoding state into the type system, | |
1281 | are also available in Rust. These patterns have different trade-offs. Although | |
1282 | you might be very familiar with object-oriented patterns, rethinking the | |
1283 | problem to take advantage of Rust’s features can provide benefits, such as | |
1284 | preventing some bugs at compile time. Object-oriented patterns won’t always be | |
1285 | the best solution in Rust due to certain features, like ownership, that | |
1286 | object-oriented languages don’t have. | |
1287 | ||
1288 | ## Summary | |
1289 | ||
1290 | No matter whether or not you think Rust is an object-oriented language after | |
1291 | reading this chapter, you now know that you can use trait objects to get some | |
1292 | object-oriented features in Rust. Dynamic dispatch can give your code some | |
1293 | flexibility in exchange for a bit of runtime performance. You can use this | |
1294 | flexibility to implement object-oriented patterns that can help your code’s | |
1295 | maintainability. Rust also has other features, like ownership, that | |
1296 | object-oriented languages don’t have. An object-oriented pattern won’t always | |
1297 | be the best way to take advantage of Rust’s strengths, but is an available | |
1298 | option. | |
1299 | ||
1300 | Next, we’ll look at patterns, which are another of Rust’s features that enable | |
1301 | lots of flexibility. We’ve looked at them briefly throughout the book but | |
1302 | haven’t seen their full capability yet. Let’s go! |