SOLID Principles in Rust: Theory and Practice for Developers

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: SOLID Principles in Rust — Why Is This Needed?

SOLID is a set of five fundamental principles of object-oriented programming that help create flexible, maintainable, and scalable code. Although Rust is not a classic OOP language (it does not have class inheritance in the usual sense), its ownership system, traits, and generics allow for brilliant implementation of all SOLID principles. Understanding these principles in the context of Rust helps write code that is easy to test, refactor, and extend.

In this article, we will analyze each of the five SOLID principles with practical examples in Rust. You will see how trait replaces interfaces, how Box<dyn Trait> helps with dependency inversion, and how to avoid common mistakes.



1. Single Responsibility Principle (SRP) in Rust

Single Responsibility Principle (SRP): each module, struct, or function should have only one reason to change. In Rust, this means a struct should be responsible for one entity, rather than mixing business logic, data handling, and output.



Example of SRP Violation

struct User {    name: String,    email: String,}

impl User { // Mixing business logic and persistence fn save_to_db(&self) { println!("Saving {} to DB", self.name); } fn send_email(&self, message: &str) { println!("Sending email to {}: {}", self.email, message); }}

Here, the User struct is responsible for user data, persistence, and sending emails. This violates SRP.



Correct SRP Implementation

struct User {    name: String,    email: String,}

struct UserRepository;impl UserRepository { fn save(&self, user: &User) { println!("Saving {} to DB", user.name); }}

struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { println!("Sending email to {}: {}", user.email, message); }}

Now each struct has a single responsibility: User — data, UserRepository — persistence, EmailService — sending. The code has become more modular and testable.



2. Open/Closed Principle (OCP) via Traits

Open/Closed Principle (OCP): software entities should be open for extension but closed for modification. In Rust, this is ideally implemented using trait and dynamic dispatch.



Example without OCP

fn calculate_area(shape: &str, params: &[f64]) -> f64 {    match shape {        "circle" => std::f64::consts::PI * params[0] * params[0],        "rectangle" => params[0] * params[1],        _ => panic!("Unknown shape"),    }}

When adding a new shape (e.g., triangle), the function must be modified.



Correct Approach with OCP

trait Shape {    fn area(&self) -> f64;}

struct Circle { radius: f64 }impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}

struct Rectangle { width: f64, height: f64 }impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}

// Function is closed for modification but open for extensionfn print_area(shape: &dyn Shape) { println!("Area: {}", shape.area());}

Now, to add a triangle, you only need to create a new struct and implement the trait Shape. The print_area function requires no changes.



3. Liskov Substitution Principle (LSP) and Inheritance via Traits

Liskov Substitution Principle (LSP): objects in a program should be replaceable with instances of

Blogs

Book Recommendations