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