Pourquoi l'architecture en Rust n'est pas un luxe, mais une nécessité
Lorsque vous écrivez en Rust, le système de types et le borrow checker vous offrent déjà 80% de protection contre les erreurs. Mais la sécurité mémoire n'est que la fondation. Si vous construisez une architecture chaotique dessus, tôt ou tard le projet se transformera en labyrinthe de dépendances mutuelles, où la modification d'un module en casse dix autres.
Les modèles architecturaux en Rust ne sont pas une mode ni une relique du monde POO. Ce sont des solutions éprouvées à des problèmes typiques : comment créer un objet qui ne violera pas les invariants ? Comment lier des composants pour pouvoir les tester individuellement ? Comment organiser la logique pour pouvoir l'étendre sans modifier le code existant ?
Dans cet article, nous allons analyser trois groupes de modèles (création, structurels, comportementaux) avec des exemples réels en Rust. Pas de théorie abstraite — seulement du code que vous pouvez appliquer immédiatement.
Modèles de création : créer des objets correctement
Builder — le sauveur des constructeurs à 10 paramètres
En Rust, il n'y a pas de surcharge de constructeurs. Si une structure a plus de 3-4 champs, écrire un seul constructeur pour tous les cas signifie condamner l'utilisateur à lire une longue signature avec Option et Default. Le Builder résout ce problème élégamment.
struct ServerConfig { host: String, port: u16, workers: u8, enable_tls: bool, max_connections: u32,}
struct ServerConfigBuilder { host: Option<String>, port: Option<u16>, workers: Option<u8>, enable_tls: Option<bool>, max_connections: Option<u32>,}
impl ServerConfigBuilder { fn new() -> Self { Self { host: None, port: None, workers: None, enable_tls: None, max_connections: None, } }
fn host(mut self, host: &str) -> Self { self.host = Some(host.to_string()); self }
fn port(mut self, port: u16) -> Self { self.port = Some(port); self }
fn workers(mut self, workers: u8) -> Self { self.workers = Some(workers); self }
fn enable_tls(mut self, enable: bool) -> Self { self.enable_tls = Some(enable); self }
fn max_connections(mut self, max: u32) -> Self { self.max_connections = Some(max); self }
fn build(self) -> Result<ServerConfig, String> { Ok(ServerConfig { host: self.host.unwrap_or_else(|| "127.0.0.1".to_string()), port: self.port.unwrap_or(8080), workers: self.workers.unwrap_or(4), enable_tls: self.enable_tls.unwrap_or(false), max_connections: self.max_connections.unwrap_or(100), }) }}
// Utilisationlet config = ServerConfigBuilder::new() .host("0.0.0.0") .port(443) .enable_tls(true) .build() .expect("Invalid config");Pourquoi cela fonctionne-t-il en Rust ? Le modèle Builder s'intègre parfaitement avec le système de possession : la chaîne de méthodes consomme et retourne self, et le build final retourne soit l'objet prêt, soit une erreur de validation. Pas de références mutables ni de conditions de concurrence.
Fabrique — quand le type de retour dépend du paramètre
Imaginez que vous devez analyser différents formats : JSON, YAML, TOML. Au lieu d'un match à chaque endroit, utilisons une méthode fabrique.
trait Parser { fn parse(&self, input: &str) -> Result<serde_json::Value, String>;}
struct JsonParser;impl Parser for JsonParser { fn parse(&self, input: &str) -> Result<serde_json::Value, String> { serde_json::from_str(input).map_err(|e| e.to_string()) }}
struct YamlParser;impl Parser for YamlParser { fn parse(&self, input: &str) -> Result<serde_json::