Design Patterns in Rust: Advanced Level with Examples
\n\nDesign patterns are proven solutions to typical problems that help create flexible, maintainable, and scalable architecture. In Rust, with its ownership, borrowing, and trait system, many classic patterns take on a unique implementation. In this article, we will examine advanced design patterns that are particularly effective in the Rust ecosystem.
\n\n1. Advanced Builder
\n\nThe classic Builder pattern in Rust is often implemented through a chain of methods returning &mut Self. However, at an advanced level, we use a typed Builder with state types to ensure correctness at compile time.
Problem: controlling mandatory fields
\nA regular Builder does not guarantee that the user has filled in all mandatory fields. We might forget to call .build() or fail to set a critically important parameter.
Solution: Builder with phantom types
\nuse std::marker::PhantomData;\n\n// State types for fields\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 // Now the compiler will not allow calling build() without setting both fields\n let user = UserBuilder::new()\n .username("alice")\n .password("secret123")\n .build();\n println!("User: {}", user.username);\n}\n\nAdvantage: the "forgot field" error is detected at compile time, not at runtime.
\n\n2. State Pattern via Traits
\n\nThe State pattern allows an object to change its behavior when its internal state changes. In Rust, this is elegantly implemented using enums and traits.
\n\nImplementation with 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!("Starting connection...");\n self.state = ConnectionState::Connecting;\n }\n ConnectionState::Connecting => {\n printl