Паттерны проектирования в Rust: продвинутый уровень с примерами
Паттерны проектирования — это проверенные решения типовых задач, которые помогают создавать гибкую, поддерживаемую и масштабируемую архитектуру. В Rust, с его системой владения, заимствования и типажами, многие классические паттерны приобретают уникальную реализацию. В этой статье мы разберём продвинутые паттерны проектирования, которые особенно эффективны в экосистеме Rust.
1. Продвинутый Builder (Строитель)
Классический паттерн Builder в Rust часто реализуется через цепочку методов, возвращающих &mut Self. Однако на продвинутом уровне мы используем типизированный Builder с типами-состояниями для обеспечения корректности на этапе компиляции.
Проблема: контроль обязательных полей
Обычный Builder не гарантирует, что пользователь заполнил все обязательные поля. Мы можем забыть вызвать .build() или не задать критически важный параметр.
Решение: Builder с типами-фантомами
use std::marker::PhantomData;
// Типы-состояния для полей
struct UsernameNotSet;
struct UsernameSet;
struct PasswordNotSet;
struct PasswordSet;
struct UserBuilder<U, P> {
username: Option<String>,
password: Option<String>,
_username: PhantomData<U>,
_password: PhantomData<P>,
}
impl UserBuilder<UsernameNotSet, PasswordNotSet> {
pub fn new() -> Self {
Self {
username: None,
password: None,
_username: PhantomData,
_password: PhantomData,
}
}
}
impl<P> UserBuilder<UsernameNotSet, P> {
pub fn username(mut self, name: &str) -> UserBuilder<UsernameSet, P> {
self.username = Some(name.to_string());
UserBuilder {
username: self.username,
password: self.password,
_username: PhantomData,
_password: PhantomData,
}
}
}
impl<U> UserBuilder<U, PasswordNotSet> {
pub fn password(mut self, pwd: &str) -> UserBuilder<U, PasswordSet> {
self.password = Some(pwd.to_string());
UserBuilder {
username: self.username,
password: self.password,
_username: PhantomData,
_password: PhantomData,
}
}
}
impl UserBuilder<UsernameSet, PasswordSet> {
pub fn build(self) -> User {
User {
username: self.username.expect("Username must be set"),
password: self.password.expect("Password must be set"),
}
}
}
pub struct User {
pub username: String,
pub password: String,
}
fn main() {
// Теперь компилятор не даст вызвать build() без установки обоих полей
let user = UserBuilder::new()
.username("alice")
.password("secret123")
.build();
println!("User: {}", user.username);
}
Преимущество: ошибка "забыл поле" обнаруживается на этапе компиляции, а не в рантайме.
2. Паттерн State (Состояние) через трейты
Паттерн State позволяет объекту менять своё поведение при изменении внутреннего состояния. В Rust это элегантно реализуется через перечисления (enum) и трейты.
Реализация с enum
enum ConnectionState {
Disconnected,
Connecting,
Connected,
}
struct Connection {
state: ConnectionState,
}
impl Connection {
pub fn new() -> Self {
Self { state: ConnectionState::Disconnected }
}
pub fn connect(&mut self) {
match self.state {
ConnectionState::Disconnected => {
println!("Начинаем подключение...");
self.state = ConnectionState::Connecting;
}
ConnectionState::Connecting => {
println!("Уже подключаемся, ждите.");
}
ConnectionState::Connected => {
println!("Уже подключены.");
}
}
}
pub fn disconnect(&mut self) {
match self.state {
ConnectionState::Connected => {
println!("Отключаемся...");
self.state = ConnectionState::Disconnected;
}
_ => println!("Не в состоянии Connected, отключение невозможно."),
}
}
}
fn main() {
let mut conn = Connection::new();
conn.connect(); // Начинаем подключение
conn.connect(); // Уже подключаемся
conn.disconnect(); // Отключаемся
}
Продвинутая версия с трейтами
trait State {
fn connect(self: Box<Self>) -> Box<dyn State>;
fn disconnect(self: Box<Self>) -> Box<dyn State>;
fn send_data(&self, data: &str);
}
struct Disconnected;
struct Connecting;
struct Connected {
session_id: u32,
}
impl State for Disconnected {
fn connect(self: Box<Self>) -> Box<dyn State> {
println!("Подключаемся...");
Box::new(Connecting)
}
fn disconnect(self: Box<Self>) -> Box<dyn State> {
println!("Уже отключены.");
self
}
fn send_data(&self, _data: &str) {
println!("Не можем отправить данные: не подключены.");
}
}
impl State for Connecting {
fn connect(self: Box<Self>) -> Box<dyn State> {
println!("Уже в процессе подключения.");
self
}
fn disconnect(self: Box<Self>) -> Box<dyn State> {
println!("Отменяем подключение.");
Box::new(Disconnected)
}
fn send_data(&self, _data: &str) {
println!("Подключение ещё не завершено.");
}
}
impl State for Connected {
fn connect(self: Box<Self>) -> Box<dyn State> {
println!("Уже подключены.");
self
}
fn disconnect(self: Box<Self>) -> Box<dyn State> {
println!("Отключаемся...");
Box::new(Disconnected)
}
fn send_data(&self, data: &str) {
println!("Отправляем данные '{}' через сессию {}", data, self.session_id);
}
}
struct BetterConnection {
state: Box<dyn State>,
}
impl BetterConnection {
fn new() -> Self {
BetterConnection { state: Box::new(Disconnected) }
}
fn connect(&mut self) {
let old_state = std::mem::replace(&mut self.state, Box::new(Disconnected));
self.state = old_state.connect();
}
fn disconnect(&mut self) {
let old_state = std::mem::replace(&mut self.state, Box::new(Disconnected));
self.state = old_state.disconnect();
}
fn send(&self, data: &str) {
self.state.send_data(data);
}
}
fn main() {
let mut conn = BetterConnection::new();
conn.send("hello"); // Не можем
conn.connect();
conn.send("hello"); // Подключение не завершено
// Здесь бы мы дождались подключения, но для примера пропустим
}