RustBrock/Characteristics of OO Languages.md
2025-04-02 15:37:00 -06:00

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)