Patrons de conception en Rust avec exemples
Les patrons de conception sont des solutions éprouvées aux problèmes courants du développement logiciel. En Rust, grâce à son système unique de possession, d'emprunt et de durée de vie, l'implémentation des patrons GoF classiques nécessite souvent une approche particulière. Dans cet article, nous analyserons plusieurs patrons populaires à l'aide d'exemples de code Rust réel.
Pourquoi Rust change-t-il l'approche des patrons ?
Contrairement aux langages avec ramasse-miettes (Java, C#), Rust impose des règles strictes sur la gestion de la mémoire. Cela rend certaines implémentations classiques (par exemple, Singleton via un état mutable global) dangereuses ou inefficaces. Cependant, Rust propose ses propres alternatives élégantes, basées sur les types et les traits.
1. Builder — construction pas à pas des objets
Builder est l'un des patrons les plus utilisés en Rust. Il est idéal pour configurer des structures complexes, surtout lorsqu'il y a de nombreux champs optionnels.
Exemple : création d'une requête HTTP
struct Request { method: String, url: String, headers: Vec<(String, String)>, body: Option<String>,}
struct RequestBuilder { method: String, url: String, headers: Vec<(String, String)>, body: Option<String>,}
impl RequestBuilder { fn new(method: &str, url: &str) -> Self { Self { method: method.to_string(), url: url.to_string(), headers: Vec::new(), body: None, } }
fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
fn body(mut self, body: &str) -> Self { self.body = Some(body.to_string()); self }
fn build(self) -> Request { Request { method: self.method, url: self.url, headers: self.headers, body: self.body, } }}
fn main() { let req = RequestBuilder::new("POST", "https://api.example.com/data") .header("Content-Type", "application/json") .header("Authorization", "Bearer token123") .body(r#"{"key": "value"}"#) .build();
println!("URL: {}", req.url); println!("Headers count: {}", req.headers.len());}Avantages de l'approche en Rust
- Sécurité : l'objet n'est créé qu'après l'appel à
build()— une configuration incomplète est impossible. - Lisibilité : la chaîne de méthodes est intuitivement compréhensible.
- Sécurité des types : le compilateur vérifie les types à chaque étape.
2. Strategy — choix d'algorithme à la volée
Strategy permet de modifier le comportement d'un objet à l'exécution. En Rust, cela s'implémente via les traits et la dispatch dynamique (Box avec traits) ou statique (génériques).
Exemple : tri de données
trait SortStrategy { fn sort(&self, data: &mut [i32]);}
struct BubbleSort;struct QuickSort;
impl SortStrategy for BubbleSort { fn sort(&self, data: &mut [i32]) { // Implémentation très simple du tri à bulles for i in 0..data.len() { for j in 0..data.len() - 1 - i { if data[j] > data[j + 1] { data.swap(j, j + 1); } } } }}
impl SortStrategy for QuickSort { fn sort(&self, data: &mut [i32]) { // Tri rapide (simplifié) if data.len() > 1 { let pivot = data.len() / 2; data.sort(); // Utilisation de la fonction intégrée pour l'exemple } }}
struct Sorter { strategy: Box<dyn SortStrategy>,}
impl Sorter { fn new(strategy: Box<dyn SortStrategy>) ->