Introduction : Pourquoi SOLID est-il important en Rust ?
SOLID est un ensemble de cinq principes fondamentaux de conception 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'y a pas d'héritage de classes au sens traditionnel), ces principes sont applicables ici grâce aux traits, aux structures, aux génériques et à la gestion de la mémoire. Dans cet article, nous analyserons chaque principe SOLID avec des exemples de code en Rust, montrerons comment les implémenter et expliquerons pourquoi ils améliorent votre code.
1. Single Responsibility Principle (SRP) : Principe de responsabilité unique
Essence : Chaque module, structure ou fonction ne doit avoir qu'une seule raison de changer.
En Rust, cela est souvent violé lorsqu'une même structure gère à la fois le stockage des données, leur sérialisation et la logique métier. La bonne approche consiste à séparer les responsabilités.
Mauvais exemple (violation du SRP)
struct User { name: String, email: String,}
impl User { fn save_to_db(&self) { // logique de sauvegarde en base }
fn send_email(&self, message: &str) { // logique d'envoi d'email }}Ici, User est responsable à la fois du stockage et de l'envoi — deux raisons de changer.
Bon exemple (respect du SRP)
struct User { name: String, email: String,}
struct UserRepository;impl UserRepository { fn save(&self, user: &User) { /* ... */ }}
struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { /* ... */ }}Maintenant, chaque structure a une seule responsabilité : User — les données, UserRepository — la sauvegarde, EmailService — l'envoi.
2. Open-Closed Principle (OCP) : Principe d'ouverture/fermeture
Essence : Les entités logicielles doivent être ouvertes à l'extension, mais fermées à la modification.
En Rust, cela est réalisé à l'aide de traits et de génériques. Vous pouvez ajouter de nouveaux comportements sans modifier le code existant.
Exemple : Système de remises
Supposons que nous ayons une fonction de calcul de prix avec remise. Au lieu d'ajouter de nouveaux types de remises via des if-else, utilisons un trait :
trait Discount { fn apply(&self, price: f64) -> f64;}
struct NoDiscount;impl Discount for NoDiscount { fn apply(&self, price: f64) -> f64 { price }}
struct SeasonalDiscount(f64);impl Discount for SeasonalDiscount { fn apply(&self, price: f64) -> f64 { price * (1.0 - self.0) }}
fn calculate_price(price: f64, discount: &dyn Discount) -> f64 { discount.apply(price)}Maintenant, pour ajouter une nouvelle remise (par exemple, BlackFridayDiscount), nous n'avons pas besoin de modifier calculate_price — il suffit d'implémenter le trait Discount.
3. Liskov Substitution Principle (LSP) : Principe de substitution de Barbara Liskov
Essence : Les objets des sous-types (en Rust — les types implémentant un trait) doivent être interchangeables avec les objets du type de base sans compromettre la correction du programme.
En Rust, cela signifie que si vous avez un trait et plusieurs de ses implémentations, le code qui fonctionne avec dyn Trait ou des génériques ne doit pas se casser lors de la substitution de l'une d'elles.
Exemple de violation du LSP
trait Bird { fn fly(&self) -> String;}
struct Sparrow;impl Bird for Sparrow { fn fly(&self) -> String { "Sparrow flies".to_string() }}
struct Penguin;impl Bird for Penguin { fn fly(&self) -> String { panic!("Penguins can't fly!") }}Si nous remplaçons Sparrow par Penguin, le code se cassera. La solution est de ne pas inclure de méthodes qui ne peuvent pas être implémentées par tous les sous-types. Il est préférable de séparer les traits :
trait CanFly { fn fly(&self) -> Stri