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: SOLID and Rust — a Perfect Combination

\n

SOLID principles are fundamental rules of object-oriented design that help create flexible, maintainable, and scalable code. Although Rust is not a purely object-oriented language (it uses traits and structs instead of classes), SOLID principles are fully applicable to it. In this article, we will break down each of the five principles using Rust code examples, showing how they help build reliable systems.

\n

Throughout the article, you will see how Rust, with its ownership system, traits, and generics, makes implementing SOLID natural and powerful. We will start with Single Responsibility and end with Dependency Inversion, providing ready-to-use examples.

\n\n

1. Single Responsibility Principle (SRP)

\n

Essence: Each module, class, or function should have only one reason to change. In Rust, this means a struct should be responsible for one task.

\n\n

Bad Example (SRP Violation)

\n
struct Report {\n    title: String,\n    content: String,\n}\n\nimpl Report {\n    fn generate(&self) -> String {\n        format!("# {}\\n\\n{}", self.title, self.content)\n    }\n\n    fn save_to_file(&self, path: &str) -> std::io::Result<()> {\n        std::fs::write(path, self.generate())?;\n        Ok(())\n    }\n\n    fn send_email(&self, recipient: &str) {\n        println!("Sending email to {} with report", recipient);\n    }\n}
\n

Here, the Report struct is responsible for generation, saving, and sending — three different reasons to change. This violates SRP.

\n\n

Good Example (SRP Compliance)

\n
struct Report {\n    title: String,\n    content: String,\n}\n\nimpl Report {\n    fn generate(&self) -> String {\n        format!("# {}\\n\\n{}", self.title, self.content)\n    }\n}\n\nstruct ReportSaver;\n\nimpl ReportSaver {\n    fn save(&self, report: &Report, path: &str) -> std::io::Result<()> {\n        std::fs::write(path, report.generate())?;\n        Ok(())\n    }\n}\n\nstruct EmailSender;\n\nimpl EmailSender {\n    fn send(&self, recipient: &str, message: &str) {\n        println!("Sending email to {}: {}", recipient, message);\n    }\n}
\n

Now each struct is responsible for its own task: Report for data and generation, ReportSaver for saving, EmailSender for sending. This simplifies testing and maintenance.

\n\n

2. Open-Closed Principle (OCP)

\n

Essence: Software entities should be open for extension but closed for modification. In Rust, this is achieved through traits and generics.

\n\n

Example: Filtering System

\n
trait Filter<T> {\n    fn apply(&self, items: &[T]) -> Vec<T>;\n}\n\nstruct EvenFilter;\n\nimpl Filter<i32> for EvenFilter {\n    fn apply(&self, items: &[i32]) -> Vec<i32> {\n        items.iter().filter(|&x| x % 2 == 0).copied().collect()\n    }\n}\n\nstruct GreaterThanFilter {\n    threshold: i32,\n}\n\nimpl Filter<i32> for GreaterThanFilter {\n    fn apply(&self, items: &[i32]) -> Vec<i32> {\n        items.iter().filter(|&&x| x > self.threshold).copied().collect()\n    }\n}\n\nfn process<T: Clone, F: Filter<T>>(items: &[T], filter: F) -> Vec<T> {\n    filter.apply(items)\n}\n\nfn main() {\n    let numbers = vec![1, 2, 3, 4, 5, 6];\n    let even = process(&numbers, EvenFilter);\n    let greater = process(&numbers, GreaterThanFilter { threshold: 3 });\n    println!("Even: {:?}, Greater: {:?}", even, greater);\n}
\n

We can add new filters without modifying the existing process code. This is an implementation of OCP through trait polymorphism.

\n\n

3. Liskov Substitution Principle (LSP)

\n

Essence:

Blogs

Book Recommendations