Паттерны проектирования в Rust с примерами: от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Паттерны проектирования в Rust: от теории к практике

Паттерны проектирования — это проверенные решения типовых задач в разработке программного обеспечения. В Rust, благодаря его уникальной системе владения (ownership) и отсутствию классического ООП, многие классические паттерны приобретают новое звучание. В этой статье мы разберём четыре ключевых паттерна: Singleton, Factory, Builder и Strategy, адаптированных под идиоматичный Rust.

1. Singleton (Одиночка) в Rust

В Rust нет глобальных статических переменных в привычном смысле. Однако Singleton можно реализовать через lazy_static или OnceCell. Рассмотрим пример с std::sync::OnceLock (доступен с Rust 1.70).

Реализация с OnceLock

use std::sync::OnceLock;

static CONFIG: OnceLock<Config> = OnceLock::new();

#[derive(Debug)]
struct Config {
    db_url: String,
    port: u16,
}

fn get_config() -> &'static Config {
    CONFIG.get_or_init(|| {
        Config {
            db_url: String::from("postgres://localhost"),
            port: 5432,
        }
    })
}

fn main() {
    let cfg = get_config();
    println!("{:?}", cfg);
}

Ключевой момент: OnceLock гарантирует однократную инициализацию и потокобезопасность. Это идиоматичный способ для Rust.

2. Factory (Фабрика) через трейты

Паттерн Factory в Rust отлично реализуется с помощью трейтов. Нам не нужны абстрактные классы — достаточно определить интерфейс через трейт.

Пример: создание транспортных средств

trait Transport {
    fn deliver(&self);
}

struct Truck;
impl Transport for Truck {
    fn deliver(&self) {
        println!("Доставка грузовиком по земле");
    }
}

struct Ship;
impl Transport for Ship {
    fn deliver(&self) {
        println!("Доставка кораблём по морю");
    }
}

struct LogisticsFactory;

impl LogisticsFactory {
    fn create_transport(route_type: &str) -> Box<dyn Transport> {
        match route_type {
            "land" => Box::new(Truck),
            "sea" => Box::new(Ship),
            _ => panic!("Неизвестный тип маршрута"),
        }
    }
}

fn main() {
    let transport = LogisticsFactory::create_transport("sea");
    transport.deliver();
}

Преимущество: Rust не требует наследования — полиморфизм достигается через трейты и Box<dyn Trait>.

3. Builder (Строитель) с типизированной цепочкой

Builder в Rust используется повсеместно, особенно для структур с множеством опциональных параметров. Реализуем его с помощью типизированной цепочки методов, возвращающих &mut Self.

Пример: создание HTTP-запроса

#[derive(Debug, Default)]
struct HttpRequest {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
    body: Option<String>,
}

struct HttpRequestBuilder {
    request: HttpRequest,
}

impl HttpRequestBuilder {
    fn new(url: &str) -> Self {
        HttpRequestBuilder {
            request: HttpRequest {
                url: url.to_string(),
                method: "GET".to_string(),
                ..Default::default()
            },
        }
    }

    fn method(&mut self, method: &str) -> &mut Self {
        self.request.method = method.to_string();
        self
    }

    fn header(&mut self, key: &str, value: &str) -> &mut Self {
        self.request.headers.push((key.to_string(), value.to_string()));
        self
    }

    fn body(&mut self, body: &str) -> &mut Self {
        self.request.body = Some(body.to_string());
        self
    }

    fn build(&self) -> HttpRequest {
        // Здесь можно добавить валидацию
        HttpRequest { ..self.request.clone() }
    }
}

fn main() {
    let req = HttpRequestBuilder::new("https://api.example.com")
        .method("POST")
        .header("Content-Type", "application/json")
        .body(r#"{"key": "value"}"#)
        .build();

    println!("{:?}", req);
}

Важно: Мы используем &mut Self для цепочечного вызова. Альтернатива — потреблять self и возвращать Self (move-семантика).

4. Strategy (Стратегия) через замыкания

Паттерн Strategy в Rust элегантно реализуется с помощью замыканий или трейтов. Рассмотрим пример с замыканиями — это наиболее идиоматичный способ.

Пример: сортировка с разными стратегиями

struct Sorter<T> {
    data: Vec<T>,
    strategy: Box<dyn Fn(&T, &T) -> std::cmp::Ordering>,
}

impl<T> Sorter<T> {
    fn new(data: Vec<T>, strategy: Box<dyn Fn(&T, &T) -> std::cmp::Ordering>) -> Self {
        Sorter { data, strategy }
    }

    fn sort(&mut self) {
        self.data.sort_by(&self.strategy);
    }

    fn result(&self) -> &[T] {
        &self.data
    }
}

fn main() {
    let numbers = vec![3, 1, 4, 1, 5, 9];

    // Стратегия: сортировка по возрастанию
    let mut sorter = Sorter::new(
        numbers,
        Box::new(|a, b| a.cmp(b)),
    );
    sorter.sort();
    println!("По возрастанию: {:?}", sorter.result());

    // Стратегия: сортировка по убыванию
    let numbers = vec![3, 1, 4, 1, 5, 9];
    let mut sorter = Sorter::new(
        numbers,
        Box::new(|a, b| b.cmp(a)),
    );
    sorter.sort();
    println!("По убыванию: {:?}", sorter.result());
}

Почему это круто: Замыкания в Rust — объекты первого класса. Они могут захватывать окружение, что делает стратегии гибкими без создания отдельных структур.

Заключение

Мы рассмотрели четыре фундаментальных паттерна проектирования в Rust. Главный вывод: Rust не требует слепого копирования GoF-паттернов из C++/Java. Вместо этого используйте:

  • Singleton — OnceLock или lazy_static
  • Factory — трейты и Box<dyn Trait>
  • <

Блоги

Рекомендаций по книгам