SOLID Principles
Obsidian Help
The most widely referenced design principles are SOLID, introduced by Robert C. Martin.
[!important]
These principles guide object-oriented design but are broadly applicable across paradigms.
Single Responsibility Principle
[!definition]
A module/struct should have only one reason to change.
[!note]
Each unit should focus on a single responsibility or concern.
struct User {
username: String,
}
impl User {
fn save(&self) {
// database logic
}
fn log_activity(&self) {
// logging logic
}
}
The bad design mixes data, persistence and logic.
struct User {
username: String,
}
struct UserRepository;
impl UserRepository {
fn save(user: &User) {
// database logic
}
}
struct Logger;
impl Logger {
fn log(message: &str) {
// logging logic
}
}
User
+username: String
uses logs via
UserRepository Logger
+save(user: User) +log(message: String)
Open-Closed Principle Obsidian Help
[!definition]
Software entities should be open for extension but closed for modification.
[!note]
You should be able to add new behaviour without changing existing code.
fn calculate_discount(user_type: &str) -> f64 {
if user_type == "regular" {
0.1
} else if user_type == "premium" {
0.2
} else {
0.0
}
}
The bad design requires modification for every new type.
trait Discount {
fn get_discount(&self) -> f64;
}
struct Regular;
struct Premium;
impl Discount for Regular {
fn get_discount(&self) -> f64 { 0.1 }
}
impl Discount for Premium {
fn get_discount(&self) -> f64 { 0.2 }
}
fn print_discount<T: Discount>(user: T) {
println!("{}", user.get_discount());
}
Checkout
+calculate(discount: Discount)
«interface»
Discount
+getDiscount() : : f64
RegularUser PremiumUser
+getDiscount() : : f64 +getDiscount() : : f64
Liskov Substitution Principle
[!definition]
Objects of a superclass should be replaceable with objects of its subclasses without breaking correctness.
[!note] Obsidian Help
Derived classes must honour the behaviour contract of the base class.
trait Bird {
fn fly(&self);
}
struct Penguin;
impl Bird for Penguin {
fn fly(&self) {
panic!("Penguins can't fly!");
}
}
This is kinda impossible to do in Rust but this breaks.
trait Bird {}
trait FlyingBird: Bird {
fn fly(&self);
}
struct Sparrow;
struct Penguin;
impl Bird for Sparrow {}
impl Bird for Penguin {}
impl FlyingBird for Sparrow {
fn fly(&self) {
println!("Flying");
}
}
«interface»
FlyingBird «interface»
Bird
+fly()
Sparrow Penguin
Interface Segregation Principle
[!definition]
Clients should not be forced to depend on interfaces they do not use.
[!note]
Prefer small, specific interfaces over large, general ones.
trait Machine {
fn print(&self);
fn scan(&self);
fn fax(&self);
}
This forces unnecessary implementation.
trait Printer {
fn print(&self);
} Obsidian Help
trait Scanner {
fn scan(&self);
}
struct SimplePrinter;
impl Printer for SimplePrinter {
fn print(&self) {
println!("Printing...");
}
}
«interface» «interface» «interface»
Fax Scanner Printer
+fax() +scan() +print()
MultiFunctionPrinter
SimplePrinter
+print()
+scan() +print()
+fax()
Dependency Inversion Principle
[!definition]
High-level modules should not depend on low-level modules. Both should depend on abstractions.
[!note]
Use interfaces/abstractions instead of concrete implementations.
struct MySQLDatabase;
impl MySQLDatabase {
fn save(&self, data: &str) {}
}
struct UserService {
db: MySQLDatabase,
}
This makes the design tightly coupled.
trait Database {
fn save(&self, data: &str);
}
struct MySQLDatabase;
impl Database for MySQLDatabase {
fn save(&self, data: &str) {
println!("Saved to MySQL");
}
}
struct UserService<T: Database> {
db: T,
}
impl<T: Database> UserService<T> { Obsidian Help
fn process(&self) {
[Link]("user data");
}
}
UserService
-db: Database
+process()
«interface»
Database
+save(data: String)
MySQLDatabase PostgresDatabase
+save(data: String) +save(data: String)