Introduction: SOLID and Rust — a Perfect Combination
\nSOLID 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.
\nThroughout 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\n1. Single Responsibility Principle (SRP)
\nEssence: 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\nBad Example (SRP Violation)
\nstruct 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}\nHere, the Report struct is responsible for generation, saving, and sending — three different reasons to change. This violates SRP.
Good Example (SRP Compliance)
\nstruct 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}\nNow each struct is responsible for its own task: Report for data and generation, ReportSaver for saving, EmailSender for sending. This simplifies testing and maintenance.
2. Open-Closed Principle (OCP)
\nEssence: Software entities should be open for extension but closed for modification. In Rust, this is achieved through traits and generics.
\n\nExample: Filtering System
\ntrait 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}\nWe can add new filters without modifying the existing process code. This is an implementation of OCP through trait polymorphism.
3. Liskov Substitution Principle (LSP)
\nEssence:
Blogs
Book Recommendations
Read also