OOP (Object Oriented Programming)
- Characterstics of OOP:
- Objects contain data and behaviour:
- Programs should make up of objects.
- An object packages both data and it's methods.
- Rust offers
struct
,enum
andimpl
blocks to provide this characterstic.
- Encapsulation:
- When the implementation details are hidden from the code that is using the object.
- The only way to use an object is through it's public API.
- This enables the programmer to change and refactor an object’s internals without needing to change the code that uses the object.
- Rust encapsulates everything by default and offers
pub
keyword to make things public.
- Inheritance:
- When an object can inherit from another object’s definition, so that it can use it's parent object's data and behavior without defining them again.
- Rust doesn't offers inheritance between
struct
, but hastrait
where you can use default methods that is both reusable and can be overridden. - The reasons to use inheritance are: code reusability and polymorphism.
- Polymorphism means that you don't need to explicitly define the type in code, but can be detected during runtime. It is useful when two types share same characterstics.
- Rust offers polymorphism in a more general manner. It offers
generics
to generalize the accepted types andtrait bounds
, to constraint the allowed types.
- Objects contain data and behaviour:
Note: People think "polymorphism is synonymous with inheritance". But it is a more general concept which means that a certain code can be referred to multiple types. It is used when two types share some common characterstics. In inheritance, those types are only subclasses.
-
Problems with Inheritance:
- It adds the risk of sharing more code than necessary.
- Subclasses are forced to share all the characterstics of the parents, even though sometimes it's not necessary or even undesired.
- Sometimes calling the functions on the subclass doen't makes sense and even cause errors.
- Due to this, some programming languages will only allow a subclass to inherit from one class, further restricting the flexibility of a program’s design.
-
Rust is different, it takes a completely different approach by using trait objects instead of inheritance.
Defining a common behaviour using trait
-
A
trait
object points to both:- An instance of a type implementing our specified trait
- A table used to look up trait methods on that type at runtime.
Property | struct or enum | trait | Objects in other languages |
---|---|---|---|
Stores Data | Yes | No | Yes |
Stores Behaviour | No | Yes | Yes |
Data and behaviour | Seperated by impl blocks. | Combined | Combined |
Uses | Store same items together | Store common behaviour and allow abstraction | Store same items and their common behavior |
-
An example:
-
Problem: Let's say initially we have components such as
Button
andImage
that may use a common functionality to draw on the screen. It's possible that someday programmers want to introduce one more component namedSelectBox
. So, what we'll end up with are different types of structures that wants to use a common functionality. -
Solution: We can invent a common function named
draw()
, which will have different implementations for different types of components. -
How to build: We'll initialise a
trait
that can be shared among various components and astruct
that can hold these compoenents. -
The
trait
will look like this: -
We can build a
struct
that holds the components that implements theDraw
: -
The difference with the alternative implementation using trait bounds in
struct
is that it restricts us to aScreen
instance that has a list of components all of typeButton
or all of typeTextField
. At compile time, the definitions will be monomorphized. -
Programmers can now create new components like this:
-
Users of this library can now use it like this:
use gui::{Button, Screen}; fn main() { let screen = Screen { components: vec![ Box::new(SelectBox { width: 75, height: 10, options: vec![ String::from("Yes"), String::from("Maybe"), String::from("No"), ], }), Box::new(Button { width: 50, height: 10, label: String::from("OK"), }), ], }; screen.run(); }
-
This concept is similar to the concept like duck typing: if it walks like a duck and quacks like a duck, then it must be a duck!
-
Use Cases:
- Generics with trait bounds: If you’ll only ever have homogeneous collections. For Example, all elements of vector will be of type
Button
. Box<dyn T>
: You can use heterogeneous collections. For Example, elements can be a mix ofButton
,TextField
etc.
- Generics with trait bounds: If you’ll only ever have homogeneous collections. For Example, all elements of vector will be of type
-
-
Static Dispatch vs Dynamic Dispatch:
Static Dispatch | Dynamic Dispatch |
---|---|
Concrete types are decided at compile time. | The compiler emits code that at runtime will figure out which method to call. |
Compiler writes some new code for various concrete types. | At runtime, it is decided whether a selected type can follow the requirements. |
When we use trait bounds on generics, static dispatch happens. | When we want to perform dynamic dispatch, we can use the dyn keyword. |
No runtime cost is added. | Some runtime cost is added. |
The State Pattern
-
The state pattern is an object-oriented design pattern.
-
The current state is stored inside the struct, along with it's value(s).
-
There are state objects, you can create a new object by implementing this trait.
-
The state pattern is built such that, methods defined on state objects will cause changes in the
Post
, but the methods defined onPost
will have no idea what these changes will look like. Hence, state objects will encapsulate behaviour changes from the main struct. -
You can look at it's complete implementation over here.
-
Some downsides of State Pattern:
- Extra Modifications: If we add a new state, we'll need to modify other states too. It's due to the reason that one state can only make transitions to another state.
- Code Duplication: It leads us to write common code inside state objects, as we cannot write directly in trait's default implementation because traits don't know about the concrete type.
-
There's another implementation of state pattern in Rust. It doesn't follow the classic OOP pattern, as we'll require to store the object in new variable, whenever a state transition will happen. Here's the code.
-