DEV Community

Igor Yusupov
Igor Yusupov

Posted on

How to Replace OOP in Rust?

Introduction

First, let's recall the main principles of OOP:

  • Inheritance
  • Encapsulation
  • Polymorphism

While there are no issues with encapsulation in Rust, developers who have worked with OOP languages like Python might face difficulties implementing the remaining principles. This is especially relevant for those who decided to rewrite a project from a language like Python/Kotlin to Rust.

Inheritance

What is inheritance? Inheritance is a mechanism that allows extending and overriding the functionality of a type. Let's look at each idea separately.

Extending Functionality

Let's consider an example in Python.

class Employee:
    def __init__(self, name: str) -> None:
        self.name = name

    def say_name(self) -> None:
        print(self.name)


class Programmer(Employee):
    pass


if __name__ == "__main__":
    programmer = Programmer("Jack")
    programmer.say_name()
Enter fullscreen mode Exit fullscreen mode

Extension means that the child class will have all the functionality of the parent. However, this is easily addressed with composition. How would it look in Rust:

struct Employee {
    name: String,
}

impl Employee {
    fn say_name(&self) {
        println!("{}", self.name);
    }
}

struct Programmer {
    employee: Employee,
}

impl Programmer {
    fn say_name(&self) {
        self.employee.say_name();
    }
}

fn main() {
    let programmer = Programmer {
        employee: Employee {
            name: "Jack".to_string(),
        },
    };
    programmer.say_name();
}

Enter fullscreen mode Exit fullscreen mode

It may seem that there is some code duplication here. However, the key advantage is that the child object doesn't need to inherit all the functionality from the parent, thus avoiding the creation of superclasses. Moreover, Rust has crates that minimize code duplication.

Overriding Functionality

Let's look at another example in Python

from abc import ABC, abstractmethod


class Employee(ABC):
    @abstractmethod
    def work(self) -> None:
        pass


class Programmer(Employee):
    def work(self) -> None:
        print("*coding...*")


class ProductManager(Employee):
    def work(self) -> None:
        print("*doing nothing*")


if __name__ == "__main__":
    programmer = Programmer()
    product_manager = ProductManager()

    programmer.work()
    product_manager.work()
Enter fullscreen mode Exit fullscreen mode

I intentionally made the Employee class abstract. This is one of the interesting features in Python, allowing you to create something akin to interfaces for child classes.

In Rust, this is solved more elegantly with traits. Let's see how it would look in Rust:

struct Programmer {}

struct ProductManager {}

trait Employee {
    fn work(&self);
}

impl Employee for Programmer {
    fn work(&self) {
        println!("*coding...*");
    }
}

impl Employee for ProductManager {
    fn work(&self) {
        println!("*doing nothing*");
    }
}

fn main() {
    let programmer = Programmer {};
    let product_manager = ProductManager {};

    programmer.work();
    product_manager.work();
}
Enter fullscreen mode Exit fullscreen mode

Additionally, traits can have default implementations for functions to avoid code duplication if the same behavior is needed for multiple structures.
Defining functionality through interfaces is also more elegant because there's no need to inherit all the functionality again. You can assign functionality "pointwise" to different structures, and a single structure can implement multiple different traits.

Polymorphism

Let's first define what polymorphism is. Polymorphism is a mechanism where a function or structure can work with different data types that implement a common interface.

In Python, this is done quite easily, just specify the base class as the type of the object (although given that you can pass anything as an argument, you might not even need to do this ๐Ÿ˜…):

from abc import ABC, abstractmethod


class Employee(ABC):
    @abstractmethod
    def work(self) -> None:
        pass


class Programmer(Employee):
    def work(self) -> None:
        print("*coding...*")


class ProductManager(Employee):
    def work(self) -> None:
        print("*doing nothing*")


def make_work(worker: Employee):
    worker.work()


if __name__ == "__main__":
    programmer = Programmer()
    product_manager = ProductManager()

    make_work(programmer)
    make_work(product_manager)

Enter fullscreen mode Exit fullscreen mode

In Rust, there are several ways to achieve this. I will talk about the two most commonly used methods.
For virtual calls in Rust, there is a special mechanism using the dyn keyword. How would this code look in Rust:

struct Programmer {}

struct ProductManager {}

trait Employee {
    fn work(&self);
}

impl Employee for Programmer {
    fn work(&self) {
        println!("*coding...*");
    }
}

impl Employee for ProductManager {
    fn work(&self) {
        println!("*doing nothing*");
    }
}

fn make_work(worker: Box<dyn Employee>) {
    worker.work();
}

fn main() {
    let programmer = Programmer {};
    let product_manager = ProductManager {};

    make_work(Box::new(programmer));
    make_work(Box::new(product_manager));
}
Enter fullscreen mode Exit fullscreen mode

But this is not the only way. By avoiding pointer indirection, we can achieve better performance. To avoid this, we can use enum.
Performance is higher in this case as it uses simple pattern matching instead of pointer indirection. Let's look at an example:

enum Employee {
    Programmer,
    ProductManager,
}

impl Employee {
    fn work(&self) {
        match *self {
            Employee::Programmer => println!("*coding...*"),
            Employee::ProductManager => println!("*doing nothing*"),
        }
    }
}

fn make_work(worker: Employee) {
    worker.work();
}

fn main() {
    let programmer = Employee::Programmer;
    let product_manager = Employee::ProductManager;

    make_work(programmer);
    make_work(product_manager);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Rust has all the necessary tools to rewrite an old project that was written using OOP principles. Moreover, if you use traits correctly, the result will be much more elegant. Additionally, inheritance in OOP can be a bad pattern as child classes often inherit attributes and methods that they don't use.

Top comments (0)