Introduction : Les principes SOLID en Rust — pourquoi en avons-nous besoin ?
SOLID est un ensemble de cinq principes fondamentaux de la programmation orientée objet qui aident à créer un code flexible, maintenable et évolutif. Bien que Rust ne soit pas un langage OOP classique (il n'a pas d'héritage de classes au sens traditionnel), son système de possession, de traits et de génériques permet d'implémenter brillamment tous les principes SOLID. Comprendre ces principes dans le contexte de Rust aide à écrire un code facile à tester, à refactoriser et à étendre.
Dans cet article, nous allons analyser chacun des cinq principes SOLID avec des exemples pratiques en Rust. Vous verrez comment trait remplace les interfaces, comment Box<dyn Trait> aide avec l'inversion de dépendances, et comment éviter les erreurs typiques.
1. Principe de responsabilité unique (SRP) en Rust
Single Responsibility Principle (SRP) : chaque module, structure ou fonction ne doit avoir qu'une seule raison de changer. En Rust, cela signifie qu'une structure doit être responsable d'une seule entité, sans mélanger la logique métier, la gestion des données et l'affichage.
Exemple de violation du SRP
struct User { name: String, email: String,}
impl User { // Mélange de la logique métier et de la sauvegarde fn save_to_db(&self) { println!("Sauvegarde de {} dans la base de données", self.name); } fn send_email(&self, message: &str) { println!("Envoi d'email à {} : {}", self.email, message); }}Ici, la structure User est responsable à la fois des données utilisateur, de la sauvegarde et de l'envoi d'emails. C'est une violation du SRP.
Implémentation correcte du SRP
struct User { name: String, email: String,}
struct UserRepository;impl UserRepository { fn save(&self, user: &User) { println!("Sauvegarde de {} dans la base de données", user.name); }}
struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { println!("Envoi d'email à {} : {}", user.email, message); }}Maintenant, chaque structure a une seule responsabilité : User pour les données, UserRepository pour le stockage, EmailService pour l'envoi. Le code est devenu plus modulaire et testable.
2. Principe d'ouverture/fermeture (OCP) via les traits
Open/Closed Principle (OCP) : les entités logicielles doivent être ouvertes à l'extension, mais fermées à la modification. En Rust, cela s'implémente idéalement avec trait et la dispatch dynamique.
Exemple sans OCP
fn calculate_area(shape: &str, params: &[f64]) -> f64 { match shape { "circle" => std::f64::consts::PI * params[0] * params[0], "rectangle" => params[0] * params[1], _ => panic!("Forme inconnue"), }}Lors de l'ajout d'une nouvelle forme (par exemple, un triangle), il faudrait modifier la fonction.
Approche correcte avec OCP
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64 }impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}
struct Rectangle { width: f64, height: f64 }impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
// Fonction fermée à la modification, mais ouverte à l'extensionfn print_area(shape: &dyn Shape) { println!("Aire : {}", shape.area());}Maintenant, pour ajouter un triangle, il suffit de créer une nouvelle structure et d'implémenter trait Shape. La fonction print_area ne nécessite aucune modification.
3. Principe de substitution de Liskov (LSP) et héritage via les traits
Liskov Substitution Principle (LSP) : les objets d'un programme doivent être remplaçables par des instances de leurs sous-types sans altérer la correction du programme. En Rust, cela se réalise via les traits et le polymorphisme.