Design Patterns en Rust : Guide Pratique avec Exemples
Les design patterns sont des solutions éprouvées à des problèmes de développement typiques. En Rust, en raison de son système de propriété unique et de son modèle mémoire, l'implémentation des patterns classiques diffère souvent des langages avec ramasse-miettes. Dans cet article, nous aborderons trois patterns clés : Singleton, Builder et Strategy, adaptés aux spécificités de Rust.
1. Pattern Singleton en Rust : État Global Sans unsafe
Singleton garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès global à celle-ci. En Rust, la crate once_cell ou le std::sync::OnceLock stable (depuis Rust 1.70) est idéal pour cela.
Implémentation avec OnceLock
use std::sync::OnceLock;
pub struct Logger { level: String,}
impl Logger { pub fn global() -> &'static Logger { static INSTANCE: OnceLock<Logger> = OnceLock::new(); INSTANCE.get_or_init(|| Logger { level: "info".to_string(), }) }
pub fn log(&self, msg: &str) { println!("[{}] {}", self.level, msg); }}
fn main() { let logger = Logger::global(); logger.log("Hello, Rust!"); // Logger::global() retournera la même instance}Avantages : Sécurité (pas de unsafe), initialisation paresseuse, sécurité des threads. Inconvénient — l'état global complique les tests.
2. Pattern Builder : Construction d'Objets Complexes
Builder sépare la construction d'un objet complexe de sa représentation. En Rust, il est particulièrement utile pour les structures avec de nombreux champs optionnels.
Exemple : Configuration d'un Client HTTP
pub struct HttpClient { url: String, timeout: u64, headers: Vec<(String, String)>,}
pub struct HttpClientBuilder { url: String, timeout: Option<u64>, headers: Vec<(String, String)>,}
impl HttpClientBuilder { pub fn new(url: &str) -> Self { HttpClientBuilder { url: url.to_string(), timeout: None, headers: vec![], } }
pub fn timeout(mut self, secs: u64) -> Self { self.timeout = Some(secs); self }
pub fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
pub fn build(self) -> HttpClient { HttpClient { url: self.url, timeout: self.timeout.unwrap_or(30), headers: self.headers, } }}
fn main() { let client = HttpClientBuilder::new("https://api.example.com") .timeout(10) .header("Authorization", "Bearer token123") .build(); println!("URL du client : {}", client.url);}Important : L'utilisation de self (pas &self) dans les méthodes du builder évite les clonages et rend l'API plus ergonomique.
3. Pattern Strategy : Algorithmes comme Stratégies Interchangeables
Strategy définit une famille d'algorithmes, encapsule chacun d'eux et les rend interchangeables. En Rust, cela est implémenté via les traits et le dispatch dynamique (Box<dyn Trait>).
Exemple : Système de Tri
trait SortStrategy { fn sort(&self, data: &mut [i32]);}
struct BubbleSort;impl SortStrategy for BubbleSort { fn sort(&self, data: &mut [i32]) { let n = data.len(); for i in 0..n { for j in 0..n - i - 1 { if data[j] > data[j + 1] { data.swap(j, j + 1); } } } }}
struct QuickSort;impl SortStrategy for QuickSort { fn sort(&self, da