Паттерны проектирования в 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 предотвращает утечки памяти