From 28d0efb644d18e8d104c2e813c8cdce50d040d3d Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Wed, 18 Oct 2017 15:39:10 -0400 Subject: [PATCH] Propagate nostarch changes to src --- second-edition/src/ch17-00-oop.md | 9 +- second-edition/src/ch17-01-what-is-oo.md | 184 +++---- second-edition/src/ch17-02-trait-objects.md | 362 ++++++------- .../src/ch17-03-oo-design-patterns.md | 489 ++++++++++-------- 4 files changed, 535 insertions(+), 509 deletions(-) diff --git a/second-edition/src/ch17-00-oop.md b/second-edition/src/ch17-00-oop.md index 9e9dd29f4..d4a6c857f 100644 --- a/second-edition/src/ch17-00-oop.md +++ b/second-edition/src/ch17-00-oop.md @@ -2,7 +2,10 @@ Object-Oriented Programming is a way of modeling programs that originated with Simula in the 1960s and became popular with C++ in the 1990s. There are many -competing definitions for what OOP is: under some definitions, Rust is -object-oriented; under other definitions, Rust is not. In this chapter, we’ll +competing definitions for what counts as OOP, and under some definitions, Rust +is object-oriented; under other definitions, it is not. In this chapter, we’ll explore some characteristics that are commonly considered to be object-oriented -and how those characteristics translate to idiomatic Rust. +and how those characteristics translate to idiomatic Rust. We’ll then show you +how to implement an object-oriented design pattern in Rust and discuss the +tradeoffs of doing so versus implementing a solution using some of Rust’s +strengths instead. diff --git a/second-edition/src/ch17-01-what-is-oo.md b/second-edition/src/ch17-01-what-is-oo.md index 9a3c7b461..a314141b5 100644 --- a/second-edition/src/ch17-01-what-is-oo.md +++ b/second-edition/src/ch17-01-what-is-oo.md @@ -1,15 +1,23 @@ ## What Does Object-Oriented Mean? -There isn’t consensus in the programming community about the features a -language needs to have in order to be called object-oriented. Rust is -influenced by many different programming paradigms; we explored the features it -has that come from functional programming in Chapter 13. Some of the -characteristics that object-oriented programming languages tend to share are -objects, encapsulation, and inheritance. Let’s take a look at what each of -those mean and whether Rust supports them. +There’s no consensus in the programming community about what features a +language needs in order to be called object-oriented. Rust is influenced by +many different programming paradigms including OOP; we explored, for example, +the features that came from functional programming in Chapter 13. Arguably, +object-oriented programming languages do tend to share certain common +characteristics, namely objects, encapsulation, and inheritance. Let’s take a +look at what each of those mean and whether Rust supports them. ### Objects Contain Data and Behavior + + + The book “Design Patterns: Elements of Reusable Object-Oriented Software,” colloquially referred to as “The Gang of Four book,” is a catalog of object-oriented design patterns. It defines object-oriented programming in this @@ -22,27 +30,27 @@ way: Under this definition, then, Rust is object-oriented: structs and enums have data and `impl` blocks provide methods on structs and enums. Even though structs and enums with methods aren’t *called* objects, they provide the same -functionality that objects do, using the Gang of Four’s definition of objects. +functionality, under the Gang of Four’s definition of objects. ### Encapsulation that Hides Implementation Details Another aspect commonly associated with object-oriented programming is the idea -of *encapsulation*: the implementation details of an object aren’t accessible -to code using that object. The only way to interact with an object is through -the public API the object offers; code using the object should not be able to -reach into the object’s internals and change data or behavior directly. -Encapsulation enables changing and refactoring an object’s internals without +of *encapsulation*: that the implementation details of an object aren’t +accessible to code using that object. The only way to interact with an object +therefore is through its public API; code using the object should not be able +to reach into the object’s internals and change data or behavior directly. This +enables the programmer to change and refactor an object’s internals without needing to change the code that uses the object. -As we discussed in Chapter 7, we can use the `pub` keyword to decide what -modules, types, functions, and methods in our code should be public, and by -default, everything is private. For example, we can define a struct -`AveragedCollection` that has a field containing a vector of `i32` values. The -struct can also have a field that knows the average of the values in the vector -so that whenever anyone wants to know the average of the values that the struct -has in its vector, we don’t have to compute it on-demand. `AveragedCollection` -will cache the calculated average for us. Listing 17-1 has the definition of -the `AveragedCollection` struct: +We discussed an example of this in Chapter 7: We can use the `pub` keyword to +decide what modules, types, functions, and methods in our code should be +public, and by default everything else is private. For example, we can define a +struct `AveragedCollection` that has a field containing a vector of `i32` +values. The struct can also have a field that contains the average of the +values in the vector, meaning the average doesn’t have to be computed on-demand +whenever anyone needs it. In other words, `AveragedCollection` will cache the +calculated average for us. Listing 17-1 has the definition of the +`AveragedCollection` struct: Filename: src/lib.rs @@ -57,11 +65,11 @@ pub struct AveragedCollection { maintains a list of integers and the average of the items in the collection. -Note that the struct itself is marked `pub` so that other code may use this -struct, but the fields within the struct remain private. This is important in -this case because we want to ensure that whenever a value is added or removed -from the list, we also update the average. We do this by implementing `add`, -`remove`, and `average` methods on the struct as shown in Listing 17-2: +The struct itself is marked `pub` so that other code may use it, but the fields +within the struct remain private. This is important in this case because we +want to ensure that whenever a value is added or removed from the list, the +average is also updated. We do this by implementing `add`, `remove`, and +`average` methods on the struct as shown in Listing 17-2: Filename: src/lib.rs @@ -102,90 +110,88 @@ impl AveragedCollection { `add`, `remove`, and `average` on `AveragedCollection` The public methods `add`, `remove`, and `average` are the only way to modify an -instance of a `AveragedCollection`. When an item is added to `list` using the -`add` method or removed using the `remove` method, the implementations of those -methods call the private `update_average` method that takes care of updating -the `average` field as well. Because the `list` and `average` fields are -private, there’s no way for external code to add or remove items to the `list` -field directly, which could cause the `average` field to get out of sync. The -`average` method returns the value in the `average` field, which allows -external code to read the `average` but not modify it. +instance of `AveragedCollection`. When an item is added to `list` using the +`add` method or removed using the `remove` method, the implementations of each +call the private `update_average` method that takes care of updating the +`average` field as well. + +We leave the `list` and `average` fields private so that there’s no way for +external code to add or remove items to the `list` field directly, otherwise +the `average` field might become out of sync. The `average` method returns the +value in the `average` field, allowing external code to read the `average` but +not modify it. Because we’ve encapsulated the implementation details of `AveragedCollection`, we can easily change aspects like the data structure in the future. For instance, we could use a `HashSet` instead of a `Vec` for the `list` field. As long as the signatures of the `add`, `remove`, and `average` public methods -stay the same, code using `AveragedCollection` wouldn’t need to change. This -wouldn’t necessarily be the case if we exposed `list` to external code: -`HashSet` and `Vec` have different methods for adding and removing items, so -the external code would likely have to change if it was modifying `list` -directly. +stay the same, code using `AveragedCollection` wouldn’t need to change. If we +made `list` public instead, this wouldn’t necessarily be the case: `HashSet` +and `Vec` have different methods for adding and removing items, so the external +code would likely have to change if it was modifying `list` directly. If encapsulation is a required aspect for a language to be considered -object-oriented, then Rust meets that requirement. Using `pub` or not for -different parts of code enables encapsulation of implementation details. +object-oriented, then Rust meets that requirement. The option to use `pub` or +not for different parts of code enables encapsulation of implementation details. ### Inheritance as a Type System and as Code Sharing -*Inheritance* is a mechanism that some programming languages provide whereby an -object can be defined to inherit from another object’s definition, thus gaining -the parent object’s data and behavior without having to define those again. -Inheritance is a characteristic that is part of some people’s definitions of -what an OOP language is. +*Inheritance* is a mechanism whereby an object can inherit from another +object’s definition, thus gaining the parent object’s data and behavior without +you having to define them again. If a language must have inheritance to be an object-oriented language, then -Rust is not object-oriented. There is not a way to define a struct that -inherits from another struct in order to gain the parent struct’s fields and -method implementations. However, if you’re used to having inheritance in your -programming toolbox, there are other solutions in Rust depending on the reason -you want to use inheritance. +Rust is not. There is no way to define a struct that inherits the parent +struct’s fields and method implementations. However, if you’re used to having +inheritance in your programming toolbox, there are other solutions in Rust +depending on your reasons for using inheritance. -There are two main reasons to reach for inheritance. The first is to be able to -re-use code: once a particular behavior is implemented for one type, -inheritance can enable re-using that implementation for a different type. Rust -code can be shared using default trait method implementations instead, which we -saw in Listing 10-15 when we added a default implementation of the `summary` -method on the `Summarizable` trait. Any type implementing the `Summarizable` -trait would have the `summary` method available on it without any further code. -This is similar to a parent class having an implementation of a method, and a -child class inheriting from the parent class also having the implementation of -the method due to the inheritance. We can also choose to override the default -implementation of the `summary` method when we implement the `Summarizable` -trait, which is similar to a child class overriding the implementation of a -method inherited from a parent class. +There are two main reasons to reach for inheritance. The first is for re-use of +code: you can implement particular behavior for one type, and inheritance +enables you to re-use that implementation for a different type. Rust code can +be shared using default trait method implementations instead, which we saw in +Listing 10-15 when we added a default implementation of the `summary` method on +the `Summarizable` trait. Any type implementing the `Summarizable` trait would +have the `summary` method available on it without any further code. This is +similar to a parent class having an implementation of a method, and an +inheriting child class then also having the implementation of the method. We +can also choose to override the default implementation of the `summary` method +when we implement the `Summarizable` trait, similar to a child class overriding +the implementation of a method inherited from a parent class. -The second reason to use inheritance is with the type system: to express that a -child type can be used in the same places that the parent type can be used. -This is also called *polymorphism*, which means that multiple objects can be -substituted for each other at runtime if they have the same shape. +The second reason to use inheritance relates to the type system: to enable a +child type to be used in the same places as the parent type. This is also +called *polymorphism*, which means that multiple objects can be substituted for +each other at runtime if they share certain characteristics. + + + -> While many people use “polymorphism” to describe inheritance, it’s actually -> a specific kind of polymorphism, called “sub-type polymorphism.” There are -> other forms as well; a generic parameter with a trait bound in Rust is -> also polymorphism, more specifically “parametric polymorphism.” The exact -> details between the different kinds of polymorphism aren’t crucial here, -> so don’t worry too much about the details: just know that Rust has multiple +> Polymorphism +> +> Many people use “polymorphism” to describe inheritance itself, but what we’re +> discussing here is actually a specific kind of polymorphism, called “sub-type +> polymorphism.” There are other forms as well; a generic parameter with a +> trait bound in Rust is “parametric polymorphism,” for example. The exact +> details of the different kinds of polymorphism aren’t crucial here, so don’t +> worry too much about them: just know that Rust has multiple > polymorphism-related features, unlike many OOP languages. -To support this sort of pattern, Rust has *trait objects* so that we can -specify that we would like values of any type, as long as the values implement -a particular trait. - Inheritance has recently fallen out of favor as a programming design solution -in many programming languages. Using inheritance to re-use some code can -require more code to be shared than you actually need. Subclasses shouldn’t -always share all characteristics of their parent class, but inheritance means -the subclass gets all of its parent’s data and behavior. This can make a -program’s design less flexible, and creates the possibility of calling methods -on subclasses that don’t make sense or cause errors since the methods don’t -apply to the subclass but must be inherited from the parent class. In addition, -some languages only allow a subclass to inherit from one class, further -restricting the flexibility of a program’s design. +in many programming languages because it’s often at risk of sharing more code +than needs be. Subclasses shouldn’t always share all characteristics of their +parent class, but will do so with inheritance. This can make a program’s design +less flexible, and introduces the possibility of calling methods on subclasses +that don’t make sense or that cause errors because the methods don’t actually +apply to the subclass. Some languages will also only allow a subclass to +inherit from one class, further restricting the flexibility of a program’s +design. -For these reasons, Rust chose to take a different approach with trait objects +For these reasons, Rust chose to take a different approach, using trait objects instead of inheritance. Let’s take a look at how trait objects enable polymorphism in Rust. diff --git a/second-edition/src/ch17-02-trait-objects.md b/second-edition/src/ch17-02-trait-objects.md index d3535181a..d0dd32cff 100644 --- a/second-edition/src/ch17-02-trait-objects.md +++ b/second-edition/src/ch17-02-trait-objects.md @@ -1,70 +1,71 @@ -## Trait Objects for Using Values of Different Types +## Using Trait Objects that Allow for Values of Different Types -In Chapter 8, we said that a limitation of vectors is that vectors can only -store elements of one type. We had an example in Listing 8-1 where we defined a -`SpreadsheetCell` enum that had variants to hold integers, floats, and text so -that we could store different types of data in each cell and still have a -vector represent a row of cells. This works for cases in which the kinds of -things we want to be able to treat interchangeably are a fixed set of types that -we know when our code gets compiled. +In Chapter 8, we mentioned that one limitation of vectors is that they can only +store elements of one type. We created a workaround in Listing 8-10 where we +defined a `SpreadsheetCell` enum that had variants to hold integers, floats, +and text. This meant we could store different types of data in each cell and +still have a vector that represented a row of cells. This is a perfectly good +solution when our interchangeable items are a fixed set of types that we know +when our code gets compiled. - +Sometimes, however, we want the user of our library to be able to extend the +set of types that are valid in a particular situation. To show how we might +achieve this, we’ll create an example Graphical User Interface tool that +iterates through a list of items, calling a `draw` method on each one to drawn +it to the screen; a common technique for GUI tools. We’re going to create a +library crate containing the structure of a GUI library called `rust_gui`. This +crate might include some types for people to use, such as `Button` or +`TextField`. On top of these, users of `rust_gui` will want to create their own +types that can be drawn on the screen: for instance, one programmer might add +an `Image`, another might add a `SelectBox`. -Sometimes we want the set of types that we use to be extensible by the -programmers who use our library. For example, many Graphical User Interface -tools have a concept of a list of items that get drawn on the screen by -iterating through the list and calling a `draw` method on each of the items. -We’re going to create a library crate containing the structure of a GUI library -called `rust_gui`. Our GUI library could include some types for people to use, -such as `Button` or `TextField`. Programmers that use `rust_gui` will want to -create more types that can be drawn on the screen: one programmer might add an -`Image`, while another might add a `SelectBox`. We’re not going to implement a -fully-fledged GUI library in this chapter, but we will show how the pieces -would fit together. +We won’t implement a fully-fledged GUI library for this example, but will show +how the pieces would fit together. At the time of writing the library, we can’t +know and define all the types other programmers will want to create. What we do +know is that `rust_gui` needs to keep track of a bunch of values that are of +different types, and it needs to be able to call a `draw` method on each of +these differently-typed values. It doesn’t need to know exactly what will +happen when we call the `draw` method, just that the value will have that +method available for us to call. -When we’re writing the `rust_gui` library, we don’t know all the types that -other programmers will want to create, so we can’t define an `enum` containing -all the types. What we do know is that `rust_gui` needs to be able to keep -track of a bunch of values of all these different types, and it needs to be -able to call a `draw` method on each of these values. Our GUI library doesn’t -need to know what will happen exactly when we call the `draw` method, just that -the value will have that method available for us to call. +To do this in a language with inheritance, we might define a class named +`Component` that has a method named `draw` on it. The other classes like +`Button`, `Image`, and `SelectBox` would inherit from `Component` and thus +inherit the `draw` method. They could each override the `draw` method to define +their custom behavior, but the framework could treat all of the types as if +they were `Component` instances and call `draw` on them. But Rust doesn’t have +inheritance, so we need another way. -In a language with inheritance, we might define a class named `Component` that -has a method named `draw` on it. The other classes like `Button`, `Image`, and -`SelectBox` would inherit from `Component` and thus inherit the `draw` method. -They could each override the `draw` method to define their custom behavior, but -the framework could treat all of the types as if they were `Component` -instances and call `draw` on them. +### Defining a Trait for Common Behavior -### Defining a Trait for the Common Behavior +To implement the behavior we want `rust_gui` to have, we’ll define a trait +named `Draw` that will have one method named `draw`. Then we can define a +vector that takes a *trait object*---this is a trait behind some sort of +pointer, such as a `&` reference or a `Box` smart pointer (we’ll talk about +the reason trait objects have to be behind a pointer in Chapter 19 in the +section on Dynamically Sized Types). We can use trait objects in place of a +generic or concrete type. Wherever we use a trait object, Rust’s type system +will ensure at compile-time that any value used in that context will implement +the trait object’s trait. This way we don’t need to know all the possible types +at compile time -In Rust, though, we can define a trait that we’ll name `Draw` and that will -have one method named `draw`. Then we can define a vector that takes a *trait -object*, which is a trait behind some sort of pointer, such as a `&` reference -or a `Box` smart pointer. We’ll talk about the reason trait objects have to -be behind a pointer in Chapter 19. + + -We mentioned that we don’t call structs and enums “objects” to distinguish -structs and enums from other languages’ objects. The data in the struct or enum -fields and the behavior in `impl` blocks is separated, as opposed to other -languages that have data and behavior combined into one concept called an -object. Trait objects *are* more like objects in other languages, in the sense -that they combine the data made up of the pointer to a concrete object with the -behavior of the methods defined in the trait. However, trait objects are -different from objects in other languages because we can’t add data to a trait -object. Trait objects aren’t as generally useful as objects in other languages: -their purpose is to allow abstraction across common behavior. +We’ve mentioned that in Rust we refrain from calling structs and enums +“objects” to distinguish them from other languages’ objects. In a struct or +enum, the data in the struct fields and the behavior in `impl` blocks is +separated, whereas in other languages the data and behavior combined into one +concept is often labeled an object. Trait objects, though, *are* more like +objects in other languages, in the sense that they combine both data and +behavior. However, trait objects differ from traditional objects in that we +can’t add data to a trait object. Trait objects aren’t as generally useful as +objects in other languages: their specific purpose is to allow abstraction +across common behavior. -A trait defines behavior that we need in a given situation. We can then use a -trait as a trait object in places where we would use a concrete type or a -generic type. Rust’s type system will ensure that any value we substitute in -for the trait object will implement the methods of the trait. Then we don’t -need to know all the possible types at compile time, and we can treat all the -instances the same way. Listing 17-3 shows how to define a trait named `Draw` -with one method named `draw`: +Listing 17-3 shows how to define a trait named `Draw` with one method named +`draw`: Filename: src/lib.rs @@ -76,13 +77,17 @@ pub trait Draw { Listing 17-3: Definition of the `Draw` trait - +This should look familiar from our discussions on how to define traits in +Chapter 10. Next comes something new: Listing 17-4 defines a struct named +`Screen` that holds a vector named `components`. This vector is of type +`Box`, which is a trait object: it’s a stand-in for any type inside a +`Box` that implements the `Draw` trait. -This should look familiar since we talked about how to define traits in -Chapter 10. Next comes something new: Listing 17-4 has the definition of a -struct named `Screen` that holds a vector named `components` that are of type -`Box`. That `Box` is a trait object: it’s a stand-in for any type -inside a `Box` that implements the `Draw` trait. + + Filename: src/lib.rs @@ -97,11 +102,11 @@ pub struct Screen { ``` Listing 17-4: Definition of the `Screen` struct with a -`components` field that holds a vector of trait objects that implement the -`Draw` trait +`components` field holding a vector of trait objects that implement the `Draw` +trait -On the `Screen` struct, we’ll define a method named `run`, which will call the -`draw` method on each of its `components` as shown in Listing 17-5: +On the `Screen` struct, we’ll define a method named `run` that will call the +`draw` method on each of its `components`, as shown in Listing 17-5: Filename: src/lib.rs @@ -126,7 +131,7 @@ impl Screen { Listing 17-5: Implementing a `run` method on `Screen` that calls the `draw` method on each component -This is different than defining a struct that uses a generic type parameter +This works differently to defining a struct that uses a generic type parameter with trait bounds. A generic type parameter can only be substituted with one concrete type at a time, while trait objects allow for multiple concrete types to fill in for the trait object at runtime. For example, we could have defined @@ -156,20 +161,20 @@ impl Screen Listing 17-6: An alternate implementation of the `Screen` struct and its `run` method using generics and trait bounds -This only lets us have a `Screen` instance that has a list of components that -are all of type `Button` or all of type `TextField`. If you’ll only ever have -homogeneous collections, using generics and trait bounds is preferable since -the definitions will be monomorphized at compile time to use the concrete types. +This restricts us to a `Screen` instance that has a list of components all of +type `Button` or all of type `TextField`. If you’ll only ever have homogeneous +collections, using generics and trait bounds is preferable since the +definitions will be monomorphized at compile time to use the concrete types. -With the definition of `Screen` that holds a component list of trait objects in -`Vec>` instead, one `Screen` instance can hold a `Vec` that contains -a `Box