mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 04:54:17 -06:00
178 lines
7.9 KiB
Markdown
178 lines
7.9 KiB
Markdown
# Characteristics of Object-Oriented Languages
|
|
|
|
There is no consensus about what features a language must have to be considered object-oriented.
|
|
|
|
Rust is influenced by many programming paradigms, including OOP.
|
|
|
|
Arguably, OOP languages share certain common characteristics, namely objects, encapsulations and ingeritance.
|
|
|
|
Lets see what each of those means and whether Rust supports it.
|
|
|
|
## Object Contain Data and Behavior
|
|
|
|
The book _Design Patterns: Elements of Reusable Object-Oriented Software_ by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley Professional, 1994).
|
|
|
|
This is colloquially referred to as _The Gang of Four_ book, is a catalog of object-oriented design patters.
|
|
|
|
It defines OOP as this:
|
|
Object-oriented programs are made up of objects.
|
|
An _object_ packages both data and the procedures that operate on that data.
|
|
The procedures are typically called _methods_ or _operations_.
|
|
Using this definition, Rust is object-oriented: structs and enumbs have data, and `impl` blocks provide methods on structs and enums.
|
|
|
|
Even though structs and enums with methods aren't _called_ obejcts, they provide the same functionaliy accound to the Gang of Four's definition of objects.
|
|
|
|
## Encapsulation that Hides Implementation Details
|
|
|
|
An aspect commonly associated with OOP is the idea of _encapsulation_.
|
|
|
|
This means that the implementation details of an object aren't accessible to code using that object.
|
|
|
|
Therefore, the only way to interact with an object is through its public API.
|
|
|
|
Code using the object shouldn't be able to reach into the object's internals and change data or behaviro directly.
|
|
|
|
This enables the programmer to change and refactor an object's internals without needing to change the code that uses the object.
|
|
|
|
We discussed previously how to control encapsulation in Ch7.
|
|
|
|
We can use the `pub` keyword to decide which modules, types, function and methods in our code should be public .
|
|
|
|
By default everything else is private.
|
|
|
|
For example, if we defined 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 te vlaues 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 of the values in the vector.
|
|
|
|
Meaning the average doesn't have to be computed on demand whenever it is needed.
|
|
|
|
`AveragedCollection` will cache the calculated average for us.
|
|
|
|
Here is the definition of the `AveragedCollection` struct
|
|
|
|
```rust
|
|
pub struct AveragedCollection {
|
|
list: Vec<i32>,
|
|
average: f64,
|
|
}
|
|
```
|
|
|
|
This struct is marked `pub` os that other code can use it.
|
|
|
|
The fields within the struct remain private.
|
|
|
|
This is improtant becuase we want to ensure that whenever a value is added or removed from the lust, the average is also updated.
|
|
|
|
We accomplish this by implementing `add`, `remove` and `average` methods on the struct.
|
|
|
|
This is shown here
|
|
|
|
```rust
|
|
impl AveragedCollection {
|
|
pub fn add(&mut self, value: i32) {
|
|
self.list.push(value);
|
|
self.update_average();
|
|
}
|
|
|
|
pub fn remove(&mut self) -> Option<i32> {
|
|
let result = self.list.pop();
|
|
match result {
|
|
Some(value) => {
|
|
self.update_average();
|
|
Some(value)
|
|
}
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
pub fn average(&self) -> f64 {
|
|
self.average
|
|
}
|
|
|
|
fn update_average(&mut self) {
|
|
let total: i32 = self.list.iter().sum();
|
|
self.average = total as f64 / self.list.len() as f64;
|
|
}
|
|
}
|
|
```
|
|
|
|
The public methods, `add`, `remove` and `average` are the only ways to access or modify data in any instance of `AverageCollection`.
|
|
|
|
When an item is added or removed to `list`, using the associated method, the implementations of each call the private `update_average` method that handles updating the `average` field as well.
|
|
|
|
We leave the `list` and `average` fields private, so that there is no way for external code to add or remove items to or form the `list` field directly.
|
|
|
|
The `average` method returns the value in the `average` field, this allows for external code to read the `average` but not modify it.
|
|
|
|
Because we have encapsulated the impleentation details of `AveragedCollection`.
|
|
|
|
We can easily change aspects, such as the data structure, in the future.
|
|
|
|
For example, we could use a `HashSet<i32>` instead of a `Vec<i32>` 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 to order to compile.
|
|
|
|
If we made `list` public instead, this wouldn't necessarily be the case
|
|
|
|
`HashSet<i32>` and `Vec<i32>` have different methods for adding and removing items, so the external code would likely have to change if it were modifying `list` directly.
|
|
|
|
If encapsulation is a required aspect for a language to be considered object-oriented, then Rust meets that requirement.
|
|
|
|
The option to use `pub` or not for different parts of code enbalbes encapsulation of implementation details
|
|
|
|
## Inheritance as a type System and as Code Sharing
|
|
|
|
_Inheritance_ is a mechanism whereby an object can inherit elements from another obejct'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 obejct-oriented, then Rust is not one.
|
|
|
|
There is no way to define a struct that inherits the parent struct fields and method implementations without using a macro.
|
|
|
|
However, if you are use to having this tool available, you cna use other solutions in Rust, depending on your reason for reaching for inheritance.
|
|
|
|
Yuo choose inheritance for main reasons.
|
|
|
|
Resue of code: you can implement particular behavior for one type, and ingeritance enables ou to reuse that implementation for a different type.
|
|
|
|
You can do this in a limited way in Rust ocde using default trait method implementations.
|
|
|
|
We saw this before with the default implemetnation of the `summarize` method on the `Summary` trait.
|
|
|
|
Any type implementing the `Summary` trait would have the `summarize` method available on it without any further code.
|
|
|
|
This is similar to a parent class having an implementation of the `summarize` method when we `implement` the `Summary` trait, which is similar to a child class overriding the implementation of a method inherited form a parent class.
|
|
|
|
The other resaon to use ingeritance relates to the type system: to enable a child to be used in the same place as the parent type.
|
|
|
|
This has another name _polymorphism_, which means that you can substitute multiple objects for each other at runtime if they share certain characteristics.
|
|
|
|
### Polymorphism
|
|
|
|
To many this is synonymous with inheritance.
|
|
|
|
But it is actually a more general concept that refers to code that can work with data of multiple tpyes.
|
|
|
|
For inheritance, those types are generally subclasses.
|
|
|
|
Rust instead use generics to abstract over different possible types and trait bounds to impose constraints on what those tpyes must provide.
|
|
|
|
This is sometimes called *bounded parametric polymorphism*.
|
|
|
|
Inheritance has recently fallen out of favor as a programming deisgn solution, becuase it is often at risk of sharing more code than necessary.
|
|
|
|
Subclasses shouldn't always share all characteristics of thier parent class but will do so with inheritance.
|
|
|
|
This can make a program's design less flexible.
|
|
|
|
This also introduces the possibility of calling methods on subclasses that don't make sense or that cause errors because the methods don't apply to the subclass.
|
|
|
|
Additionally some languages will only allow single inheritance, meaning a subclass can only inherit from one class, thus further restricting the flexibility of a program's design.
|
|
|
|
These reasons are why Rust takes the different approach of using trait objects instead of inheritance.
|
|
|
|
Next lets see how trait obejcts enable polymorphism in Rust. [Here](./Trait%20Objects%20that%20allow%20for%20Values%20of%20Different%20Types.md)
|