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

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

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

Начать курс

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

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

1. Паттерн Singleton в Rust: глобальное состояние без unsafe

Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. В Rust для этого идеально подходит крейт once_cell или стабильный std::sync::OnceLock (с Rust 1.70).

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

use std::sync::OnceLock;

pub struct Logger {
    level: String,
}

impl Logger {
    pub fn global() -> &'static Logger {
        static INSTANCE: OnceLock<Logger> = OnceLock::new();
        INSTANCE.get_or_init(|| Logger {
            level: "info".to_string(),
        })
    }

    pub fn log(&self, msg: &str) {
        println!("[{}] {}", self.level, msg);
    }
}

fn main() {
    let logger = Logger::global();
    logger.log("Привет, Rust!");
    // Logger::global() вернёт тот же экземпляр
}

Преимущества: Безопасность (нет unsafe), ленивая инициализация, потокобезопасность. Недостаток — глобальное состояние усложняет тестирование.

2. Паттерн Builder: конструирование сложных объектов

Builder отделяет конструирование сложного объекта от его представления. В Rust он особенно полезен для структур с множеством опциональных полей.

Пример: конфигурация HTTP-клиента

pub struct HttpClient {
    url: String,
    timeout: u64,
    headers: Vec<(String, String)>,
}

pub struct HttpClientBuilder {
    url: String,
    timeout: Option<u64>,
    headers: Vec<(String, String)>,
}

impl HttpClientBuilder {
    pub fn new(url: &str) -> Self {
        HttpClientBuilder {
            url: url.to_string(),
            timeout: None,
            headers: vec![],
        }
    }

    pub fn timeout(mut self, secs: u64) -> Self {
        self.timeout = Some(secs);
        self
    }

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

    pub fn build(self) -> HttpClient {
        HttpClient {
            url: self.url,
            timeout: self.timeout.unwrap_or(30),
            headers: self.headers,
        }
    }
}

fn main() {
    let client = HttpClientBuilder::new("https://api.example.com")
        .timeout(10)
        .header("Authorization", "Bearer token123")
        .build();
    println!("Client URL: {}", client.url);
}

Важно: Использование self (не &self) в методах builder'а позволяет избежать клонирования и делает API более эргономичным.

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

Strategy определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. В Rust это реализуется через трейты (traits) и динамическую диспетчеризацию (Box<dyn Trait>).

Пример: система сортировки

trait SortStrategy {
    fn sort(&self, data: &mut [i32]);
}

struct BubbleSort;
impl SortStrategy for BubbleSort {
    fn sort(&self, data: &mut [i32]) {
        let n = data.len();
        for i in 0..n {
            for j in 0..n - i - 1 {
                if data[j] > data[j + 1] {
                    data.swap(j, j + 1);
                }
            }
        }
    }
}

struct QuickSort;
impl SortStrategy for QuickSort {
    fn sort(&self, data: &mut [i32]) {
        if data.len() > 1 {
            let pivot = partition(data);
            self.sort(&mut data[..pivot]);
            self.sort(&mut data[pivot + 1..]);
        }
    }
}

fn partition(data: &mut [i32]) -> usize {
    let pivot = data.len() - 1;
    let mut i = 0;
    for j in 0..pivot {
        if data[j] <= data[pivot] {
            data.swap(i, j);
            i += 1;
        }
    }
    data.swap(i, pivot);
    i
}

struct Sorter {
    strategy: Box<dyn SortStrategy>,
}

impl Sorter {
    fn new(strategy: Box<dyn SortStrategy>) -> Self {
        Sorter { strategy }
    }

    fn sort(&self, data: &mut [i32]) {
        self.strategy.sort(data);
    }
}

fn main() {
    let mut data = vec![5, 3, 8, 1, 2];
    let sorter = Sorter::new(Box::new(QuickSort));
    sorter.sort(&mut data);
    println!("Отсортировано: {:?}", data);
}

Альтернатива: Для статической диспетчеризации используйте обобщённые типы Sorter<T: SortStrategy>. Это быстрее, но требует знания типа на этапе компиляции.

4. Паттерн Observer: реактивное уведомление

Observer определяет зависимость "один ко многим" между объектами. В Rust его можно реализовать через трейты и Weak<RefCell<Observer>> для избежания циклических ссылок.

Упрощённая реализация

use std::cell::RefCell;
use std::rc::{Rc, Weak};

trait Observer {
    fn update(&self, message: &str);
}

struct Subject {
    observers: RefCell<Vec<Weak<dyn Observer>>>,
}

impl Subject {
    fn new() -> Self {
        Subject {
            observers: RefCell::new(vec![]),
        }
    }

    fn attach(&self, observer: Weak<dyn Observer>) {
        self.observers.borrow_mut().push(observer);
    }

    fn notify(&self, message: &str) {
        let mut observers = self.observers.borrow_mut();
        observers.retain(|obs| {
            if let Some(obs) = obs.upgrade() {
                obs.update(message);
                true
            } else {
                false
            }
        });
    }
}

struct ConsoleLogger;
impl Observer for ConsoleLogger {
    fn update(&self, message: &str) {
        println!("Логгер: {}", message);
    }
}

fn main() {
    let subject = Subject::new();
    let logger = Rc::new(ConsoleLogger);
    subject.attach(Rc::downgrade(&logger));
    subject.notify("Событие произошло!");
}

Примечание: Использование Weak предотвращает утечки памяти

Блоги

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