Паттерны проектирования в Rust с примерами: продвинутый уровень
Rust — это язык, который меняет подход к проектированию. Классические паттерны из GoF часто требуют адаптации из-за строгой системы владения, времени жизни и отсутствия наследования. В этой статье мы разберем продвинутые паттерны проектирования, которые не просто работают в Rust, но используют его сильные стороны: безопасность памяти, zero-cost абстракции и мощную систему типов.
1. Паттерн Builder с проверками на этапе компиляции
Классический Builder в Rust обычно реализуется через структуру с опциональными полями. На продвинутом уровне мы можем использовать систему типов, чтобы проверять обязательные поля на этапе компиляции. Это исключает runtime-ошибки "забыл установить обязательное поле".
Реализация через Type State (состояние типа) позволяет сделать Builder безопасным и самодокументируемым.
// Состояния для Builder
struct Empty;
struct WithName(String);
struct WithAge(u8);
struct Ready(WithName, WithAge);
struct PersonBuilder<State> {
state: State,
}
impl PersonBuilder<Empty> {
pub fn new() -> Self {
PersonBuilder { state: Empty }
}
pub fn name(self, name: &str) -> PersonBuilder<WithName> {
PersonBuilder {
state: WithName(name.to_string()),
}
}
}
impl PersonBuilder<WithName> {
pub fn age(self, age: u8) -> PersonBuilder<Ready> {
PersonBuilder {
state: Ready(self.state, WithAge(age)),
}
}
}
impl PersonBuilder<Ready> {
pub fn build(self) -> Person {
Person {
name: self.state.0 .0,
age: self.state.1 .0,
}
}
}
struct Person {
name: String,
age: u8,
}
fn main() {
// Компилируется - все поля установлены
let person = PersonBuilder::new()
.name("Alice")
.age(30)
.build();
// Не скомпилируется: нет name()
// let person = PersonBuilder::new().age(30).build();
println!("{} is {} years old", person.name, person.age);
}
Преимущество: ошибки конфигурации отлавливаются на этапе компиляции, а не во время выполнения. Это особенно ценно для сложных объектов с десятками параметров.
2. Паттерн RAII и умные указатели для управления ресурсами
RAII (Resource Acquisition Is Initialization) — это не просто паттерн, а философия Rust. Каждый ресурс (память, файл, сокет, блокировка) должен быть захвачен в конструкторе и освобожден в деструкторе. Rust реализует это через трейты Drop и систему владения.
Продвинутый пример — создание RAII-обертки для мьютекса с автоматическим освобождением.
use std::ops::Deref;
use std::sync::{Arc, Mutex};
struct Guard<T> {
data: T,
}
impl<T> Drop for Guard<T> {
fn drop(&mut self) {
println!("Ресурс освобожден!");
}
}
impl<T> Deref for Guard<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
struct ResourceManager<T> {
resource: Arc<Mutex<T>>,
}
impl<T> ResourceManager<T> {
fn new(data: T) -> Self {
ResourceManager {
resource: Arc::new(Mutex::new(data)),
}
}
fn acquire(&self) -> Guard<std::sync::MutexGuard<T>> {
let guard = self.resource.lock().unwrap();
Guard { data: guard }
}
}
fn main() {
let manager = ResourceManager::new(42);
{
let guard = manager.acquire();
println!("Значение: {}", *guard);
// guard выходит из области видимости -> автоматически освобождает мьютекс
} // Печатает "Ресурс освобожден!"
println!("Мьютекс снова доступен");
}
Ключевая идея: деструктор вызывается автоматически при выходе переменной из области видимости. Это гарантирует освобождение ресурсов даже в случае паники или раннего возврата.
3. Паттерн Strategy через трейты и замыкания
В Rust паттерн Strategy элегантно реализуется через трейты или замыкания. В отличие от классического ООП, здесь нет наследования — используется статическая диспетчеризация (monomorphization) или динамическая (через Box<dyn Trait>).
Пример: система сжатия данных с разными алгоритмами.
// Трейт для стратегии сжатия
trait CompressionStrategy {
fn compress(&self, data: &[u8]) -> Vec<u8>;
fn decompress(&self, data: &[u8]) -> Vec<u8>;
}
// Конкретные стратегии
struct GzipStrategy;
impl CompressionStrategy for GzipStrategy {
fn compress(&self, data: &[u8]) -> Vec<u8> {
// Имитация сжатия
data.iter().map(|&b| b.wrapping_add(1)).collect()
}
fn decompress(&self, data: &[u8]) -> Vec<u8> {
data.iter().map(|&b| b.wrapping_sub(1)).collect()
}
}
struct NoCompression;
impl CompressionStrategy for NoCompression {
fn compress(&self, data: &[u8]) -> Vec<u8> {
data.to_vec()
}
fn decompress(&self, data: &[u8]) -> Vec<u8> {
data.to_vec()
}
}
// Контекст, использующий стратегию
struct DataCompressor<S: CompressionStrategy> {
strategy: S,
}
impl<S: CompressionStrategy> DataCompressor<S> {
fn new(strategy: S) -> Self {
DataCompressor { strategy }
}
fn process(&self, data: &[u8]) -> Vec<u8> {
self.strategy.compress(data)
}
}
// Альтернатива с замыканиями
fn process_with_closure<F>(data: &[u8], compressor: F) -> Vec<u8>
where
F: Fn(&[u8]) -> Vec<u8>,
{
compressor(data)
}
fn main() {
let data = b"Hello, Rust!";
// Статическая диспетчеризация
let gzip_compressor = DataCompressor::new(GzipStrategy);
let compressed = gzip_compressor.process(data);
println!("Сжато Gzip: {:?}", compressed);
// Динамическая диспетчеризация
let strategies: Vec<Box<dyn CompressionStrategy>> = vec![
Box::new(GzipStrategy),
Box::new(NoCompression),
];
for strategy in strategies {
let result = strategy.compress(data);
println