Modèles de conception en Rust : niveau avancé avec exemples
\n\nLes modèles de conception sont des solutions éprouvées à des problèmes typiques, qui aident à créer une architecture flexible, maintenable et évolutive. En Rust, avec son système de possession, d'emprunt et de traits, de nombreux modèles classiques acquièrent une implémentation unique. Dans cet article, nous analyserons les modèles de conception avancés qui sont particulièrement efficaces dans l'écosystème Rust.
\n\n1. Builder avancé (Constructeur)
\n\nLe modèle classique Builder en Rust est souvent implémenté via une chaîne de méthodes retournant &mut Self. Cependant, au niveau avancé, nous utilisons un Builder typé avec des types d'état pour garantir la correction à la compilation.
Problème : contrôle des champs obligatoires
\nUn Builder ordinaire ne garantit pas que l'utilisateur a rempli tous les champs obligatoires. Nous pouvons oublier d'appeler .build() ou ne pas définir un paramètre critique.
Solution : Builder avec types fantômes
\nuse std::marker::PhantomData;\n\n// Types d'état pour les champs\nstruct UsernameNotSet;\nstruct UsernameSet;\nstruct PasswordNotSet;\nstruct PasswordSet;\n\nstruct UserBuilder<U, P> {\n username: Option<String>,\n password: Option<String>,\n _username: PhantomData<U>,\n _password: PhantomData<P>,\n}\n\nimpl UserBuilder<UsernameNotSet, PasswordNotSet> {\n pub fn new() -> Self {\n Self {\n username: None,\n password: None,\n _username: PhantomData,\n _password: PhantomData,\n }\n }\n}\n\nimpl<P> UserBuilder<UsernameNotSet, P> {\n pub fn username(mut self, name: &str) -> UserBuilder<UsernameSet, P> {\n self.username = Some(name.to_string());\n UserBuilder {\n username: self.username,\n password: self.password,\n _username: PhantomData,\n _password: PhantomData,\n }\n }\n}\n\nimpl<U> UserBuilder<U, PasswordNotSet> {\n pub fn password(mut self, pwd: &str) -> UserBuilder<U, PasswordSet> {\n self.password = Some(pwd.to_string());\n UserBuilder {\n username: self.username,\n password: self.password,\n _username: PhantomData,\n _password: PhantomData,\n }\n }\n}\n\nimpl UserBuilder<UsernameSet, PasswordSet> {\n pub fn build(self) -> User {\n User {\n username: self.username.expect("Username must be set"),\n password: self.password.expect("Password must be set"),\n }\n }\n}\n\npub struct User {\n pub username: String,\n pub password: String,\n}\n\nfn main() {\n // Désormais, le compilateur ne permettra pas d'appeler build() sans définir les deux champs\n let user = UserBuilder::new()\n .username("alice")\n .password("secret123")\n .build();\n println!("User: {}", user.username);\n}\n\nAvantage : l'erreur "champ oublié" est détectée à la compilation, et non à l'exécution.
\n\n2. Modèle State (État) via les traits
\n\nLe modèle State permet à un objet de modifier son comportement lorsque son état interne change. En Rust, cela s'implémente élégamment via les énumérations (enum) et les traits.
\n\nImplémentation avec enum
\nenum ConnectionState {\n Disconnected,\n Connecting,\n Connected,\n}\n\nstruct Connection {\n state: ConnectionState,\n}\n\nimpl Connection {\n pub fn new() -> Self {\n Self { state: ConnectionState::Disconnected }\n }\n\n pub fn connect(&mut self) {\n match self.state {\n ConnectionState::Disconnected => {\n println!("Début de la connexion...");\n self.state = ConnectionState::Connecting;\n }\n ConnectionState::Connecting => {\n printl