Why Rust does not need OOP — Bob Belderbos | Developer & Team Coaching
Learning Rust? I co-run a 6-week Python to Rust cohort where you build a performant JSON parser with PyO3 bindings.
When I heard structs replace classes in Rust, I was a bit surprised. I thought, how can you do without classes? But as I started to learn Rust, I realized that structs, traits, ownership and composition help resist the temptation of OOP. In fact, Rust's approach to programming is more focused on data and behavior rather than objects.
Let's look at 5 reasons why Rust does not need OOP.
1. Composition
One major drawback of OOP is deep inheritance trees.
In classic OOP you would define class A, then class B inherits from A, then class C inherits from B, and so on. This can lead to a complex and fragile codebase where changes in one class can affect many others.
class Animal {}<br>class Dog extends Animal {}<br>class ServiceDog extends Dog {}<br>In What Rust Structs Taught Me About State Ownership I showed this example:
struct Tokenizer {<br>chars: Vecchar>,<br>position: usize,
impl Tokenizer {<br>fn advance(&mut self) -> Optionchar> {<br>let ch = self.chars.get(self.position).copied();<br>self.position += 1;<br>ch
fn peek(&self) -> Optionchar> {<br>self.chars.get(self.position).copied() // char is Copy, returns a value not a reference<br>We see a clear separation of data (the fields) and behavior (the methods). The Tokenizer struct holds the state, while the methods define how to interact with that state. This is a more flexible and modular approach than OOP's class-based design.
Rust also uses composition instead of inheritance. You can create complex types by combining simpler ones without the need for a class hierarchy.
struct Animal {}<br>struct Dog {<br>animal: Animal,<br>struct ServiceDog {<br>dog: Dog,
A great resource on this principle is Composition Over Inheritance, part of Brandon Rhodes' Python Patterns Guide.
For an example where I think OOP & inheritance went off the rails is Django's class-based views. The inheritance tree of those views is too deep making it an unpleasant API to work with, and the code so much harder to reason about. A better way is the more functional approach, see Luke Plant's Django Views — The Right Way.
2. Traits
In Python, think Protocols. In Java, think interfaces. In Rust, traits are a powerful way to define shared behavior without the need for a class hierarchy; polymorphism without inheritance.
Classic OOP:
Animal a = new Dog();<br>a.speak();<br>In Rust, you can achieve similar behavior using traits:
trait Speak {<br>fn speak(&self);
fn make_noise(x: &impl Speak) {<br>x.speak();<br>The advantage here is that you can implement the Speak trait for any type, and you don't need to have a common base class. This allows for more flexibility and code reuse.
Python has something similar with Protocols, which are part of the typing module. They allow you to define a set of methods that a class must implement, without requiring inheritance.
from typing import Protocol
class Speak(Protocol):<br>def speak(self) -> None: ...
def make_noise(x: Speak) -> None:<br>x.speak()<br>This is a more flexible alternative to ABCs (Abstract Base Classes) and allows for duck typing while still providing type safety. I wrote an article about this on Pybites.
Where OOP couples data and behavior, Rust's traits allow you to define behavior separately from data. This promotes code reuse and flexibility without the need for a rigid class structure.
Rust encourages:
struct User {}<br>trait Serialize {}<br>trait Validate {}<br>trait Persist {}<br>This is closer to the Single Responsibility Principle, the Unix philosophy of "do one thing and do it well", functional programming of data-oriented design with pure functions that operate on data, and composition over inheritance.
Hence Rust allows you to mix and match traits to create complex behavior without the need for a class hierarchy.
3. Ownership and borrowing
Many OOP patterns exist to control mutation and provide proper encapsulation. In Rust, ownership and borrowing rules ensure that data is accessed safely and efficiently.
fn process(data: Data) // takes ownership (moved in)<br>fn process(data: &Data) // borrows, read-only<br>fn process(data: &mut Data) // borrows, can mutate<br>Just by looking at the function signature in Rust, you can understand how data is being used and modified: who owns it, who mutates it, and when it goes out of scope. It eliminates the need for patterns like getters/setters, which are often used in OOP to control access to data. And these rules are enforced at compile time, not runtime.
4. Modularity
I came to the conclusion some time ago that Python's module scope is a great feature. It allows you to organize code in a way that is more flexible than OOP's class-based organization. In Rust, modules and crates provide a way to organize code without the need for classes.
Classic OOP:
public class Counter {<br>private int value;
public void increment() {<br>value += 1;<br>In Rust, you can use...