Post

Rust Notes: Object-Oriented Programming

A review of OOP in Rust.

Object-Oriented Programming in Rust

Rust is not traditionally considered an object-oriented language in the same way as Java or C++.

It does not have:

  • classes
  • traditional class inheritance
  • constructors in the traditional OOP sense
  • private/protected/public fields and methods in the same class-based model
  • virtual methods as a built-in class feature

However, Rust supports many of the important ideas associated with object-oriented programming.

Rust provides:

  • encapsulation
  • abstraction
  • polymorphism
  • interfaces through traits
  • composition
  • dynamic dispatch
  • static dispatch

The important difference is that Rust provides these features through its type system, modules, structs, enums, and traits, rather than through classes and inheritance.

A useful way to think about Rust is:

Rust supports many OOP concepts, but it does not implement OOP through classes and inheritance.


1. What Is Object-Oriented Programming?

Object-oriented programming is usually associated with a few major ideas:

  1. Encapsulation
  2. Abstraction
  3. Inheritance
  4. Polymorphism

Different languages implement these concepts differently.

For example, in Java you might have:

1
2
3
4
5
6
7
8
9
10
11
class Animal {
    public void speak() {
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("Woof");
    }
}

The class contains:

  • data
  • methods
  • visibility
  • inheritance relationships

Rust separates these concepts.

For example:

1
2
3
4
5
6
7
8
9
struct Dog {
    name: String,
}

impl Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

The data is defined by the struct:

1
2
3
struct Dog {
    name: String,
}

The methods are defined by an impl block:

1
2
3
4
5
impl Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

This separation is fundamental to Rust.


2. Structs Are Similar to Classes, But Not the Same

A Rust struct can contain data:

1
2
3
4
struct User {
    name: String,
    age: u32,
}

You can define methods using impl:

1
2
3
4
5
impl User {
    fn greet(&self) {
        println!("Hello {}", self.name);
    }
}

Then:

1
2
3
4
5
6
let user = User {
    name: String::from("Amir"),
    age: 29,
};

user.greet();

This looks similar to a class:

1
2
3
4
5
6
7
8
9
10
11
Java/C++
Class
 ├── fields
 └── methods

Rust
Struct
 └── fields

impl
 └── methods

But Rust intentionally separates the data definition from the implementation.


3. Encapsulation in Rust

Rust absolutely supports encapsulation.

The main tools are:

  • modules
  • visibility modifiers
  • private fields
  • public methods
  • traits

By default, items are private.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mod bank {
    pub struct Account {
        balance: u64,
    }

    impl Account {
        pub fn new(balance: u64) -> Self {
            Self { balance }
        }

        pub fn balance(&self) -> u64 {
            self.balance
        }

        pub fn withdraw(&mut self, amount: u64) {
            if amount <= self.balance {
                self.balance -= amount;
            }
        }
    }
}

Outside the module:

1
2
3
let mut account = bank::Account::new(100);

println!("{}", account.balance());

But this is not allowed:

1
println!("{}", account.balance);

because balance is private.

This is encapsulation.

The user of Account does not directly manipulate its internal state.

Instead, they use:

1
account.withdraw(20);

4. Rust Visibility

Rust provides several visibility mechanisms.

The most common are:

1
pub

and private visibility.

For example:

1
2
3
4
pub struct User {
    pub name: String,
    age: u32,
}

Here:

1
2
name → public
age  → private

You can also expose something only within a parent module:

1
pub(crate)

This means:

Public within the current crate.

Example:

1
2
pub(crate) fn internal_function() {
}

There are also more advanced visibility forms such as:

1
2
pub(super)
pub(in crate::some_module)

These allow more precise control over visibility.


5. Encapsulation Is Not Only About Classes

In Java, encapsulation is often associated with:

1
private int balance;

and:

1
public void withdraw(int amount)

Rust achieves the same general idea through modules and visibility:

1
2
3
4
5
6
7
8
9
pub struct Account {
    balance: u64,
}

impl Account {
    pub fn withdraw(&mut self, amount: u64) {
        // ...
    }
}

The important principle is:

Expose behavior, hide implementation details.

This is a very useful way to design Rust APIs.


6. Rust Does Not Have Constructors

Rust does not have a special constructor keyword such as:

1
2
3
4
class User {
public:
    User(int age);
};

Instead, Rust commonly uses associated functions.

1
2
3
4
5
6
7
8
9
10
struct User {
    name: String,
    age: u32,
}

impl User {
    fn new(name: String, age: u32) -> Self {
        Self { name, age }
    }
}

Then:

1
2
3
4
let user = User::new(
    String::from("Amir"),
    29,
);

The conventional name is:

1
new

but it is not a special language feature.

You could write:

1
2
3
4
5
impl User {
    fn create(name: String, age: u32) -> Self {
        Self { name, age }
    }
}

and:

1
2
3
4
let user = User::create(
    String::from("Amir"),
    29,
);

7. Methods and Associated Functions

Rust distinguishes between methods and associated functions.

A method has self:

1
2
3
4
5
impl User {
    fn greet(&self) {
        println!("Hello");
    }
}

Called with:

1
user.greet();

An associated function does not have self:

1
2
3
4
5
impl User {
    fn new(name: String) -> Self {
        Self { name }
    }
}

Called with:

1
User::new(String::from("Amir"));

This distinction is useful when thinking about constructors.


8. Inheritance in Rust

Rust does not support traditional class inheritance.

You cannot write:

1
2
struct Dog extends Animal {
}

Instead, Rust uses traits.

1
2
3
4
5
6
7
8
9
10
11
trait Animal {
    fn speak(&self);
}

struct Dog;

impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

The trait defines behavior:

1
2
3
4
5
Animal
  │
  │ implemented by
  ▼
Dog

This is different from:

1
2
3
4
5
Animal
  │
  │ inherited by
  ▼
Dog

Rust intentionally avoids traditional inheritance.


9. Why Does Rust Avoid Inheritance?

Inheritance creates a strong relationship between types.

For example:

1
2
3
4
5
6
7
Animal
  │
  ▼
Dog
  │
  ▼
SpecialDog

Over time, inheritance hierarchies can become difficult to maintain.

Rust generally favors:

Composition over inheritance.

Instead of saying:

A Dog inherits everything from Animal.

we say:

A Dog implements the behavior described by Animal.

This makes the relationship more explicit.


10. Composition in Rust

Suppose we have an engine:

1
2
3
struct Engine {
    horsepower: u32,
}

and a car:

1
2
3
struct Car {
    engine: Engine,
}

The car contains an engine.

1
2
Car
 └── Engine

This is composition.

You can then implement behavior:

1
2
3
4
5
6
7
8
impl Car {
    fn start(&self) {
        println!(
            "Starting {} HP engine",
            self.engine.horsepower
        );
    }
}

This is often preferred over inheritance.


11. Traits as Interfaces

Traits are Rust’s main mechanism for defining shared behavior.

1
2
3
trait Animal {
    fn speak(&self);
}

A type implements the interface:

1
2
3
4
5
6
7
struct Dog;

impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}

This is similar to a Java interface:

1
2
3
interface Animal {
    void speak();
}

But Rust traits are more powerful than simple interfaces.

They can contain:

  • methods
  • default implementations
  • associated types
  • associated constants
  • associated functions
  • trait bounds

12. Polymorphism in Rust

Rust supports polymorphism.

There are two major forms you should understand:

  1. Static polymorphism
  2. Dynamic polymorphism

13. Static Polymorphism

Using generics:

1
2
3
4
5
6
7
trait Animal {
    fn speak(&self);
}

fn speak<T: Animal>(animal: &T) {
    animal.speak();
}

Then:

1
2
3
4
5
let dog = Dog;
let cat = Cat;

speak(&dog);
speak(&cat);

The compiler knows the concrete type of T.

Conceptually:

1
2
3
4
5
6
7
speak::<Dog>()
    ↓
Dog::speak()

speak::<Cat>()
    ↓
Cat::speak()

This is static dispatch.

The compiler determines the implementation at compile time.


14. Dynamic Polymorphism

Rust can also perform runtime polymorphism using trait objects.

1
2
3
fn speak(animal: &dyn Animal) {
    animal.speak();
}

Now:

1
2
3
4
5
let dog = Dog;
let cat = Cat;

speak(&dog);
speak(&cat);

The concrete type is hidden behind:

1
dyn Animal

Rust uses dynamic dispatch to select the implementation.

Conceptually:

1
2
3
4
5
&dyn Animal
     │
     ├── Dog → Dog::speak()
     │
     └── Cat → Cat::speak()

15. Trait Objects

Common trait-object forms include:

1
2
3
4
&dyn Trait
Box<dyn Trait>
Rc<dyn Trait>
Arc<dyn Trait>

For example:

1
let animal: Box<dyn Animal> = Box::new(Dog);

This is useful when the concrete type should be hidden.


16. Different Types in One Collection

This is one of the most important reasons to use dynamic polymorphism.

This does not work:

1
2
3
4
let animals = vec![
    Dog,
    Cat,
];

because a Vec<T> normally contains one concrete type.

Instead:

1
2
3
4
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];

Now:

1
2
3
4
Vec<Box<dyn Animal>>
        │
        ├── Dog
        └── Cat

The collection has one type:

1
Box<dyn Animal>

while the underlying objects have different concrete types.


17. Static vs Dynamic Dispatch

The distinction is important.

Static dispatch:

1
2
3
fn process<T: Animal>(animal: &T) {
    animal.speak();
}

Dynamic dispatch:

1
2
3
fn process(animal: &dyn Animal) {
    animal.speak();
}

Think of it as:

1
2
3
4
5
6
7
8
9
10
T: Animal
   │
   └── compiler knows concrete type
       └── static dispatch


dyn Animal
   │
   └── concrete type hidden
       └── runtime dispatch

Static dispatch generally allows more compile-time optimization.

Dynamic dispatch gives runtime flexibility.


18. Traits and Abstraction

Traits allow you to depend on behavior instead of implementation.

Suppose you have:

1
2
struct PostgresDatabase;
struct InMemoryDatabase;

Instead of:

1
2
fn save(db: &PostgresDatabase) {
}

define:

1
2
3
trait Database {
    fn save(&self);
}

Then:

1
2
3
fn save<D: Database>(db: &D) {
    db.save();
}

Now the function doesn’t care whether the database is PostgreSQL, memory-based, or something else.

It only cares:

Does this type implement Database?

This is abstraction.


19. Dependency Injection

Traits are useful for dependency injection.

For example:

1
2
3
trait Storage {
    fn get(&self, key: &str) -> String;
}

Production implementation:

1
2
3
4
5
6
7
struct DatabaseStorage;

impl Storage for DatabaseStorage {
    fn get(&self, key: &str) -> String {
        String::from("real value")
    }
}

Test implementation:

1
2
3
4
5
6
7
struct MockStorage;

impl Storage for MockStorage {
    fn get(&self, key: &str) -> String {
        String::from("fake value")
    }
}

Application code:

1
2
3
4
fn process<S: Storage>(storage: &S) {
    let value = storage.get("key");
    println!("{value}");
}

Production:

1
process(&DatabaseStorage);

Testing:

1
process(&MockStorage);

The application depends on the abstraction:

1
2
3
4
        Storage trait
        /           \
       /             \
DatabaseStorage    MockStorage

rather than a concrete implementation.


20. Traits and Encapsulation Together

Traits become particularly powerful when combined with private implementation details.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mod storage {
    pub trait Storage {
        fn get(&self, key: &str) -> String;
    }

    struct DatabaseStorage {
        connection: String,
    }

    impl Storage for DatabaseStorage {
        fn get(&self, key: &str) -> String {
            // implementation
            String::from("value")
        }
    }

    pub fn create_storage() -> impl Storage {
        DatabaseStorage {
            connection: String::from("database"),
        }
    }
}

The caller can use:

1
let storage = storage::create_storage();

without knowing the concrete type.

This gives us:

1
2
3
4
5
6
7
8
9
Public API
    │
    ▼
Storage trait
    │
    ▼
Hidden implementation
    │
    └── DatabaseStorage

This is a powerful form of abstraction.


21. Opaque Types with impl Trait

Rust can hide the concrete type:

1
2
3
fn create_storage() -> impl Storage {
    DatabaseStorage
}

The caller knows:

The returned value implements Storage.

But does not need to know the exact concrete type.

This is different from:

1
Box<dyn Storage>

With:

1
impl Storage

there is one hidden concrete type.

With:

1
dyn Storage

the concrete type can vary at runtime.


22. impl Trait vs dyn Trait

Compare:

1
2
3
fn create() -> impl Animal {
    Dog
}

and:

1
2
3
4
5
6
7
fn create(condition: bool) -> Box<dyn Animal> {
    if condition {
        Box::new(Dog)
    } else {
        Box::new(Cat)
    }
}

The first returns one hidden concrete type.

The second can return different concrete types.

1
2
3
4
5
6
7
8
9
impl Animal
     │
     └── one concrete type hidden


dyn Animal
     │
     ├── Dog
     └── Cat

23. Rust’s Version of an Abstract Class

Rust does not have abstract classes.

Instead, you usually define a trait:

1
2
3
trait Animal {
    fn speak(&self);
}

Then concrete types implement it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Dog;
struct Cat;

impl Animal for Dog {
    fn speak(&self) {
        println!("Woof");
    }
}

impl Animal for Cat {
    fn speak(&self) {
        println!("Meow");
    }
}

The trait provides the abstraction.


24. Default Trait Methods

Traits can provide default behavior:

1
2
3
4
5
trait Animal {
    fn speak(&self) {
        println!("Some animal sound");
    }
}

A type can use the default:

1
2
3
struct Dog;

impl Animal for Dog {}

or override it:

1
2
3
4
5
impl Animal for Dog {
    fn speak(&self) {
        println!("Woof");
    }
}

This gives traits some functionality that resembles abstract base classes, without requiring inheritance.


25. Trait Composition

Traits can require other traits.

1
2
3
trait Animal: Display {
    fn speak(&self);
}

This means:

1
2
3
Animal
  │
  └── requires Display

An implementation must satisfy both:

1
2
3
4
5
6
7
impl Display for Dog {
    // ...
}

impl Animal for Dog {
    // ...
}

Multiple requirements are possible:

1
2
3
trait Animal: Display + Debug + Send {
    fn speak(&self);
}

This is sometimes called using supertraits.


26. Operator Overloading Through Traits

Rust’s operators are heavily trait-based.

For example:

1
a + b

uses the Add trait.

Other examples include:

1
2
3
4
5
6
7
+       Add
-       Sub
*       Mul
/       Div
==      PartialEq
<       PartialOrd
[]      Index

This demonstrates an important Rust principle:

Language behavior is often expressed through traits.


27. Trait-Based Polymorphism vs Inheritance-Based Polymorphism

In a traditional OOP language:

1
2
3
4
Animal
  │
  ├── Dog
  └── Cat

The relationship is often inheritance.

In Rust:

1
2
3
4
Animal trait
    │
    ├── Dog implements Animal
    └── Cat implements Animal

This is not inheritance.

It is a contract:

1
2
Animal:
    must provide speak()

Dog:

1
implements speak()

Cat:

1
implements speak()

The difference may seem small at first, but it leads to a different style of program design.


28. Composition Over Inheritance

Suppose we want a Car.

Instead of:

1
2
3
4
Vehicle
  │
  ▼
Car

we might use:

1
2
3
4
5
6
7
8
9
10
11
12
struct Engine {
    // ...
}

struct Wheels {
    // ...
}

struct Car {
    engine: Engine,
    wheels: Wheels,
}

Now:

1
2
3
Car
 ├── Engine
 └── Wheels

The car is composed of components.

This is often easier to modify than a deep inheritance hierarchy.


29. Traits for Capabilities

A useful Rust mental model is to treat traits as capabilities.

For example:

1
Clone

means:

This type can be cloned.

1
Debug

means:

This type has a debugging representation.

1
Display

means:

This type can be formatted for users.

1
Iterator

means:

This type can produce a sequence of values.

1
Send

means:

This type can safely be transferred to another thread.

1
Sync

means:

References to this type can safely be shared between threads.

This capability-based design is one of the most important ideas in Rust.


30. Traits Are Also Used by the Type System

Traits aren’t only an OOP-like feature.

Rust itself uses traits extensively.

For example:

1
2
fn spawn<T: Send + 'static>(value: T) {
}

Here Send is a trait used to express a concurrency property.

Similarly:

1
T: Clone

is a requirement that the type supports cloning.

And:

1
T: Iterator

means that the type provides iterator behavior.

So traits are much broader than traditional interfaces.


31. Enums and OOP

Rust enums can also be used to model polymorphic data.

For example:

1
2
3
4
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}

Then:

1
2
3
4
5
6
7
8
9
10
11
12
13
impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => {
                std::f64::consts::PI * radius * radius
            }

            Shape::Rectangle { width, height } => {
                width * height
            }
        }
    }
}

This is another Rust alternative to some traditional OOP designs.

Instead of:

1
2
3
Shape
 ├── Circle
 └── Rectangle

through inheritance, you can represent the alternatives explicitly:

1
2
3
enum Shape
 ├── Circle
 └── Rectangle

This is especially useful when the set of variants is known.


32. Enum vs Trait

This distinction is very important.

Use an enum when:

I know all possible variants.

Example:

1
2
3
4
5
enum Message {
    Text(String),
    Number(i32),
    Quit,
}

Use a trait when:

I want different types to provide the same behavior.

Example:

1
2
3
trait Renderer {
    fn render(&self);
}

Then:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct HtmlRenderer;
struct JsonRenderer;

impl Renderer for HtmlRenderer {
    fn render(&self) {
        // ...
    }
}

impl Renderer for JsonRenderer {
    fn render(&self) {
        // ...
    }
}

A useful rule:

1
2
3
4
5
6
7
8
Known alternatives
        ↓
      enum


Open-ended behavior
        ↓
      trait

33. Rust Does Not Force an OOP Style

You can write Rust in several styles.

Procedural:

1
2
3
fn calculate(a: i32, b: i32) -> i32 {
    a + b
}

Data-oriented:

1
2
3
struct User {
    name: String,
}

Trait-oriented:

1
2
3
trait Serialize {
    fn serialize(&self);
}

Functional-style:

1
2
3
4
let result = values
    .iter()
    .map(|x| x * 2)
    .collect::<Vec<_>>();

Rust is therefore often described as a multi-paradigm language.


34. A Better Mental Model for Rust OOP

Instead of thinking:

“Where is the class?”

think:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
What data do I have?
        ↓
     struct

What behavior does it have?
        ↓
     impl

What behavior should multiple types share?
        ↓
     trait

Do I know the concrete type at compile time?
        ↓
     generic / impl Trait

Do I need runtime polymorphism?
        ↓
     dyn Trait

Do I need to hide implementation details?
        ↓
     modules + visibility + traits

This mental model is much more useful than trying to translate Java classes directly into Rust.


35. Example: Designing a Storage System

Suppose we are designing a storage abstraction.

Instead of:

1
2
3
4
5
abstract class Storage
       │
       ├── S3Storage
       ├── CephStorage
       └── LocalStorage

Rust can use:

1
2
3
4
trait Storage {
    fn put(&self, key: &str, data: &[u8]);
    fn get(&self, key: &str) -> Vec<u8>;
}

Implementations:

1
2
3
4
5
struct S3Storage;

struct CephStorage;

struct LocalStorage;

Then:

1
2
3
4
5
6
7
8
9
10
impl Storage for S3Storage {
    fn put(&self, key: &str, data: &[u8]) {
        // S3 implementation
    }

    fn get(&self, key: &str) -> Vec<u8> {
        // S3 implementation
        Vec::new()
    }
}

and:

1
2
3
4
5
6
7
8
9
10
impl Storage for CephStorage {
    fn put(&self, key: &str, data: &[u8]) {
        // Ceph implementation
    }

    fn get(&self, key: &str) -> Vec<u8> {
        // Ceph implementation
        Vec::new()
    }
}

The application can depend only on:

1
Storage

rather than a particular backend.


36. Why This Style Is Useful for Systems Programming

This approach fits systems programming particularly well.

For example, you might have:

1
2
3
4
5
6
Storage
Network
Device
Backend
Transport
Scheduler

as traits describing capabilities.

Concrete implementations can then be:

1
2
3
4
5
6
CephStorage
S3Storage
VirtioDevice
VhostBackend
TcpTransport
UnixTransport

The abstraction is separated from the implementation.

This is especially useful when:

  • testing
  • supporting multiple backends
  • building libraries
  • writing virtualization software
  • implementing operating-system components
  • designing extensible infrastructure software

37. Rust’s Approach to OOP

Rust does not reject object-oriented ideas.

Instead, it separates the concepts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Traditional OOP
────────────────────────────
Class
 ├── state
 ├── methods
 ├── visibility
 ├── inheritance
 └── polymorphism


Rust
────────────────────────────
struct
 └── state

impl
 └── methods

module + pub/private
 └── encapsulation

trait
 └── shared behavior

generic
 └── static polymorphism

dyn Trait
 └── dynamic polymorphism

composition
 └── reuse of implementation

This separation gives you more control over how each concept is used.


38. The Most Important Differences from Java/C++

ConceptJava/C++Rust
ClassYesNo
StructYes, but different semanticsYes
MethodsInside classesimpl blocks
EncapsulationClass visibilityModules + visibility
InterfacesInterfaces / abstract classesTraits
InheritanceYesNo traditional inheritance
CompositionYesYes
PolymorphismVirtual methods / genericsdyn Trait / generics
Static dispatchTemplates/genericsGenerics
Dynamic dispatchVirtual methodsTrait objects
ConstructorsSpecial language featureAssociated functions
Abstract classesYesTraits
Operator overloadingLanguage-specificTraits
Multiple inheritanceLimited / supported differentlyNot traditional; traits provide composition of behavior

39. What Rust Gives You Instead of Inheritance

Instead of inheritance, Rust gives you several tools:

1
2
3
4
5
6
7
8
9
Composition
    +
Traits
    +
Generics
    +
Trait objects
    +
Enums

Together, these cover many designs that would traditionally be implemented with inheritance.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Traditional OOP:

Animal
  │
  ├── Dog
  └── Cat


Rust:

trait Animal
  │
  ├── Dog implements Animal
  └── Cat implements Animal


and/or:

enum Animal
  ├── Dog
  └── Cat

The best choice depends on whether the problem is about shared behavior or a closed set of variants.


40. Key Takeaways

Rust supports many object-oriented programming concepts, but it does not use traditional class-based OOP.

The most important ideas are:

  • struct defines data.
  • impl defines methods and associated functions.
  • Modules and visibility provide encapsulation.
  • pub exposes items outside their normal visibility boundary.
  • Private fields can protect internal state.
  • Associated functions such as new() are commonly used as constructors.
  • Traits define shared behavior and act similarly to interfaces.
  • Rust does not have traditional class inheritance.
  • Rust favors composition over inheritance.
  • Traits provide polymorphism.
  • Generic traits normally use static dispatch.
  • dyn Trait provides dynamic dispatch.
  • Box<dyn Trait> allows different concrete types to be stored together.
  • impl Trait hides a concrete type while preserving static dispatch.
  • Enums are useful when the set of possible variants is known.
  • Traits are useful when you want open-ended shared behavior.
  • Traits can provide default implementations.
  • Traits can require other traits through supertraits.
  • Traits can define associated types and associated constants.
  • Traits are also used to express capabilities such as Send, Sync, Clone, and Iterator.
  • Rust’s OOP-like features work together with ownership and borrowing rather than replacing them.

The most useful mental model is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Data
  ↓
struct

Behavior for one type
  ↓
impl

Shared behavior
  ↓
trait

Compile-time polymorphism
  ↓
generics / impl Trait

Runtime polymorphism
  ↓
dyn Trait

Encapsulation
  ↓
modules + pub/private

Reuse
  ↓
composition

The key idea is:

Rust does not try to make everything an object. It gives you separate tools for data, behavior, abstraction, encapsulation, and polymorphism, and lets you combine them.

That is why Rust can express many object-oriented designs while still being fundamentally different from class-based languages such as Java and C++.

This post is licensed under CC BY 4.0 by the author.