Introduction: Why SOLID is Important for Rust Developers
SOLID is a set of five fundamental principles of object-oriented design that help create flexible, maintainable, and scalable code. Despite Rust not being a classic OOP language, these principles are perfectly applicable thanks to the system of traits, ownership, and generics. In this article, we will break down each SOLID principle with practical examples in Rust.
You will learn how to:
- Avoid "god" structures through decomposition
- Design extensible systems using traits
- Effectively use polymorphism without inheritance
- Separate interfaces and inject dependencies
1. Single Responsibility Principle (SRP)
SRP states: "A structure or module should have only one reason to change". In Rust, this means each type should be responsible for one clearly defined task.
SRP Violation: The "God" Structure
struct Report { data: Vec<String>,}
impl Report { fn generate(&self) -> String { self.data.join(", ") }
fn save_to_file(&self, path: &str) -> std::io::Result<()> { let content = self.generate(); std::fs::write(path, content) }
fn send_email(&self, recipient: &str) { // Email sending logic println!("Sending report {} to {}", self.generate(), recipient); }}This code violates SRP: the Report structure is responsible for data generation, saving it to a file, and sending an email. Changing the saving or sending logic would require modifying the report itself.
Correct Implementation with SRP
struct Report { data: Vec<String>,}
impl Report { fn new(data: Vec<String>) -> Self { Self { data } }
fn generate(&self) -> String { self.data.join(", ") }}
struct FileSaver;
impl FileSaver { fn save(report: &Report, path: &str) -> std::io::Result<()> { std::fs::write(path, report.generate()) }}
struct EmailSender;
impl EmailSender { fn send(report: &Report, recipient: &str) { println!("Sending report {} to {}", report.generate(), recipient); }}Now each structure has one responsibility: Report — generation, FileSaver — saving, EmailSender — sending. This simplifies testing and code modification.
2. Open/Closed Principle (OCP)
OCP states: "Classes should be open for extension but closed for modification". In Rust, this is achieved through traits and generics.
Example: Payment Processing System
trait PaymentProcessor { fn process(&self, amount: f64) -> String;}
struct CreditCardProcessor;
impl PaymentProcessor for CreditCardProcessor { fn process(&self, amount: f64) -> String { format!("Processing {} via credit card", amount) }}
struct PayPalProcessor;
impl PaymentProcessor for PayPalProcessor { fn process(&self, amount: f64) -> String { format!("Processing {} via PayPal", amount) }}
struct PaymentHandler<T: PaymentProcessor> { processor: T,}
impl<T: PaymentProcessor> PaymentHandler<T> { fn execute(&self, amount: f64) -> String { self.processor.process(amount) }}Adding a new payment method (e.g., CryptoProcessor) does not require changing existing code — it is enough to implement the PaymentProcessor trait.