SOLID Principles in Rust: Theory and Practice from A to Z

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: Why is SOLID Important in Rust?

SOLID is a set of five fundamental principles of object-oriented design that help create flexible, maintainable, and scalable code. Despite the fact that Rust is not a classic OOP language (it has no class inheritance in the traditional sense), these principles are applicable here as well thanks to traits, structs, generics, and memory ownership. In this article, we will break down each SOLID principle using Rust code examples, show how to implement them, and explain why they make your code better.



1. Single Responsibility Principle (SRP)

Essence: Each module, struct, or function should have only one reason to change.

In Rust, this is often violated when a single struct is responsible for both storing data, serializing it, and handling business logic. The correct approach is to separate responsibilities.



Bad Example (SRP Violation)

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

impl User { fn save_to_db(&self) { // logic for saving to database }

fn send_email(&self, message: &str) { // logic for sending email }}

Here, User is responsible for both storage and sending — two reasons to change.



Good Example (SRP Compliance)

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

struct UserRepository;impl UserRepository { fn save(&self, user: &User) { /* ... */ }}

struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { /* ... */ }}

Now each struct has a single responsibility: User — data, UserRepository — storage, EmailService — sending.



2. Open-Closed Principle (OCP)

Essence: Software entities should be open for extension but closed for modification.

In Rust, this is achieved using traits and generics. You can add new behavior without changing existing code.



Example: Discount System

Suppose we have a function for calculating a discounted price. Instead of adding new discount types via if-else, we use a trait:



trait Discount {    fn apply(&self, price: f64) -> f64;}

struct NoDiscount;impl Discount for NoDiscount { fn apply(&self, price: f64) -> f64 { price }}

struct SeasonalDiscount(f64);impl Discount for SeasonalDiscount { fn apply(&self, price: f64) -> f64 { price * (1.0 - self.0) }}

fn calculate_price(price: f64, discount: &dyn Discount) -> f64 { discount.apply(price)}

Now, to add a new discount (e.g., BlackFridayDiscount), we don't need to change calculate_price — we simply implement the Discount trait.



3. Liskov Substitution Principle (LSP)

Essence: Subtype objects (in Rust, types implementing a trait) should be substitutable for base type objects without breaking program correctness.

In Rust, this means that if you have a trait and several of its implementations, code working with dyn Trait or generics should not break when substituting any of them.



Example of LSP Violation

trait Bird {    fn fly(&self) -> String;}

struct Sparrow;impl Bird for Sparrow { fn fly(&self) -> String { "Sparrow flies".to_string() }}

struct Penguin;impl Bird for Penguin { fn fly(&self) -> String { panic!("Penguins can't fly!") }}

If we replace Sparrow with Penguin, the code will break. The solution is not to include methods that cannot be implemented by all subtypes. It's better to split the traits:



trait CanFly {    fn fly(&self) -> Stri                

Blogs

Book Recommendations