أنماط التصميم في Rust: دليل شامل
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты строишь дом. У тебя есть куча кирпичей, досок, гвоздей и инструментов. Можно, конечно, начать класть кирпичи как попало, прибивать доски «на глаз» и каждый раз придумывать, как соединить крышу со стенами. Но если ты так сделаешь, дом, скорее всего, развалится при первой же буре. А если даже и не развалится, то перестроить в нём что-то будет невозможно — всё держится на честном слове и клее «Момент».
Опытные строители так не работают. Они используют типовые проекты. Есть проверенные схемы, как сделать фундамент, как собрать каркас, как провести электрику. Эти схемы называются паттернами (от английского pattern — «шаблон», «образец»).
В программировании — то же самое. Паттерны проектирования — это не готовый код, который можно скопировать. Это проверенные рецепты решения типовых задач. Когда ты сталкиваешься с проблемой «как удобно создавать сложные объекты?» или «как сделать, чтобы разные части программы общались, не ломая друг друга?», ты не изобретаешь велосипед — ты берёшь готовый паттерн.
Важно: Паттерны — это не магия и не обязательное требование. Это инструмент. Как молоток: можно забивать гвозди и камнем, но молотком удобнее. Паттерны делают код понятным, гибким и лёгким для изменений.
В этой статье мы разберём три самых популярных паттерна на примере языка Rust. Почему Rust? Потому что он строгий, быстрый и не прощает ошибок. Если ты научишься применять паттерны в Rust, то в любом другом языке они покажутся тебе лёгкими.
Теория: как это работает изнутри
Все паттерны делятся на три большие группы:
- Порождающие — отвечают за создание объектов. Вместо того чтобы писать
new Something()везде, мы делегируем создание специальному коду. - Структурные — отвечают за то, как классы и объекты собираются в большие структуры.
- Поведенческие — отвечают за взаимодействие между объектами.
Мы рассмотрим по одному паттерну из каждой группы:
| Название | Группа | Суть |
|---|---|---|
| Строитель (Builder) | Порождающий | Позволяет создавать сложные объекты пошагово. Вместо огромного конструктора с 20 параметрами — цепочка методов. |
| Адаптер (Adapter) | Структурный | Позволяет объектам с несовместимыми интерфейсами работать вместе. Как переходник для розетки. |
| Наблюдатель (Observer) | Поведенческий | Один объект (издатель) уведомляет множество других (подписчиков) о своих изменениях. |
Теперь разберём каждый подробно.
Строитель (Builder)
Представь, что ты заказываешь пиццу. Ты не говоришь: «Дай мне пиццу с параметрами: тесто=тонкое, сыр=моцарелла, топпинг=пепперони, размер=30см, соус=томатный». Вместо этого ты говоришь: «Мне, пожалуйста, пиццу с тонким тестом, с пепперони, без оливок». Это пошаговое конструирование.
В программировании бывают объекты, у которых много полей. Часть из них обязательна, часть — опциональна. Если сделать конструктор с 10 параметрами, код станет нечитаемым. Строитель решает эту проблему: он предоставляет методы для установки каждого параметра, а в конце — метод build(), который создаёт готовый объект.
Адаптер (Adapter)
У тебя есть старый телевизор с антенным кабелем. И есть новый HDMI-кабель от приставки. Они несовместимы. Ты берёшь переходник (адаптер), который с одной стороны — антенный штекер, с другой — HDMI. Теперь всё работает.
В коде то же самое: есть старый класс с методом old_request(), а новый код ожидает new_request(). Адаптер — это класс-прослойка, который принимает new_request(), а внутри вызывает old_request().
Наблюдатель (Observer)
Ты подписан на YouTube-канал. Когда автор выпускает новое видео, ты получаешь уведомление. Ты не дёргаешь автора каждую секунду с вопросом «Ну что, вышло?». Автор сам оповещает всех подписчиков.
В коде: есть объект-издатель (Subject), который хранит список подписчиков (Observers). Когда у издателя меняется состояние, он проходит по списку и вызывает у каждого подписчика метод update().
Синтаксис в Rust: все формы и варианты
Rust — язык с уникальной системой владения (ownership) и заимствования (borrowing). Это накладывает отпечаток на реализацию паттернов. В Rust нет классов в классическом ООП-смысле, но есть структуры (struct) и трейты (trait). Трейты — это как интерфейсы в других языках.
Давай разберём синтаксис каждого паттерна.
Строитель в Rust
В Rust Строитель часто реализуется через структуру с опциональными полями (тип Option<T>). Каждый метод-сеттер возвращает &mut Self, чтобы можно было вызывать методы цепочкой. В конце — метод build(), который возвращает Result<Product, Error> или просто Product.
// Структура, которую мы будем строить
struct Pizza {
size: u32, // размер в см
cheese: bool, // есть ли сыр
pepperoni: bool, // есть ли пепперони
sauce: String, // тип соуса
}
// Строитель
struct PizzaBuilder {
size: Option<u32>, // опционально — может быть не указано
cheese: bool, // по умолчанию false
pepperoni: bool,
sauce: Option<String>,
}
impl PizzaBuilder {
// Конструктор строителя
fn new() -> Self {
PizzaBuilder {
size: None,
cheese: false,
pepperoni: false,
sauce: None,
}
}
// Метод-сеттер для размера
fn size(&mut self, value: u32) -> &mut Self {
self.size = Some(value);
self // возвращаем ссылку на себя для цепочки
}
// Метод для сыра
fn cheese(&mut self, value: bool) -> &mut Self {
self.cheese = value;
self
}
// Метод для пепперони
fn pepperoni(&mut self, value: bool) -> &mut Self {
self.pepperoni = value;
self
}
// Метод для соуса
fn sauce(&mut self, value: &str) -> &mut Self {
self.sauce = Some(value.to_string());
self
}
// Финальный метод — создаёт Pizza
fn build(&self) -> Result<Pizza, String> {
// Проверяем, что обязательные поля заполнены
let size = self.size.ok_or("Размер не указан!")?;
let sauce = self.sauce.clone().unwrap_or_else(|| "томатный".to_string());
Ok(Pizza {
size,
cheese: self.cheese,
pepperoni: self.pepperoni,
sauce,
})
}
}
fn main() {
// Использование строителя
let pizza = PizzaBuilder::new()
.size(30)
.cheese(true)
.pepperoni(true)
.build()
.expect("Ошибка при создании пиццы");
println!("Пицца: {} см, сыр: {}, пепперони: {}, соус: {}",
pizza.size, pizza.cheese, pizza.pepperoni, pizza.sauce);
}
Адаптер в Rust
В Rust Адаптер реализуется через трейты. У нас есть старый объект с одним методом и новый интерфейс (трейт) с другим методом. Адаптер — это структура, которая хранит старый объект и реализует новый трейт.
// Старый объект — умеет работать только с XML
struct OldLogger;
impl OldLogger {
fn log_xml(&self, message: &str) {
println!("<log>{}</log>", message);
}
}
// Новый интерфейс — хотим JSON
trait NewLogger {
fn log_json(&self, message: &str);
}
// Адаптер — переходник
struct LoggerAdapter {
old_logger: OldLogger,
}
impl LoggerAdapter {
fn new(old_logger: OldLogger) -> Self {
LoggerAdapter { old_logger }
}
}
// Реализуем новый интерфейс через старый
impl NewLogger for LoggerAdapter {
fn log_json(&self, message: &str) {
// Преобразуем JSON-подобный вызов в XML
let xml_message = format!("{{"message": "{}"}}", message);
self.old_logger.log_xml(&xml_message);
}
}
fn main() {
let old = OldLogger;
let adapter = LoggerAdapter::new(old);
// Теперь можно вызывать новый метод
adapter.log_json("Привет, мир!");
}
Наблюдатель в Rust
В Rust Наблюдатель сложнее из-за правил заимствования. Мы не можем хранить список подписчиков как Vec<dyn Observer> просто так — нужно использовать Box или Rc (счётчик ссылок).
use std::rc::Rc;
use std::cell::RefCell;
// Трейт для наблюдателя
trait Observer {
fn update(&self, message: &str);
}
// Конкретный наблюдатель — пользователь
struct User {
name: String,
}
impl Observer for User {
fn update(&self, message: &str) {
println!("{} получил уведомление: {}", self.name, message);
}
}
// Издатель
struct NewsPublisher {
// Rc — счётчик ссылок, RefCell — внутренняя изменяемость
observers: Vec<Rc<RefCell<dyn Observer>>>,
}
impl NewsPublisher {
fn new() -> Self {
NewsPublisher {
observers: Vec::new(),
}
}
// Подписать наблюдателя
fn subscribe(&mut self, observer: Rc<RefCell<dyn Observer>>) {
self.observers.push(observer);
}
// Оповестить всех
fn publish(&self, news: &str) {
for observer in &self.observers {
observer.borrow().update(news);
}
}
}
fn main() {
let mut publisher = NewsPublisher::new();
// Создаём пользователей
let user1 = Rc::new(RefCell::new(User { name: "Алиса".to_string() }));
let user2 = Rc::new(RefCell::new(User { name: "Боб".to_string() }));
// Подписываем
publisher.subscribe(user1);
publisher.subscribe(user2);
// Публикуем новость
publisher.publish("В Rust вышла новая версия!");
}
Примеры кода — от простого к сложному
Теперь давай закрепим теорию на семи примерах. Каждый пример — рабочий, с объяснением.
Пример 1: Простой Строитель для чашки кофе
struct Coffee {
milk: bool,
sugar: u8,
size: String,
}
struct CoffeeBuilder {
milk: bool,
sugar: u8,
size: String,
}
impl CoffeeBuilder {
fn new() -> Self {
CoffeeBuilder {
milk: false,
sugar: 0,
size: "маленький".to_string(),
}
}
fn with_milk(&mut self) -> &mut Self {
self.milk = true;
self
}
fn with_sugar(&mut self, spoons: u8) -> &mut Self {
self.sugar = spoons;
self
}
fn size(&mut self, size: &str) -> &mut Self {
self.size = size.to_string();
self
}
fn build(&self) -> Coffee {
Coffee {
milk: self.milk,
sugar: self.sugar,
size: self.size.clone(),
}
}
}
fn main() {
let coffee = CoffeeBuilder::new()
.with_milk()
.with_sugar(2)
.size("большой")
.build();
println!("Кофе: {}, молоко: {}, сахар: {}", coffee.size, coffee.milk, coffee.sugar);
}
Пример 2: Строитель с проверкой ошибок
struct User {
name: String,
age: u8,
email: String,
}
struct UserBuilder {
name: Option<String>,
age: Option<u8>,
email: Option<String>,
}
impl UserBuilder {
fn new() -> Self {
UserBuilder { name: None, age: None, email: None }
}
fn name(&mut self, name: &str) -> &mut Self {
self.name = Some(name.to_string());
self
}
fn age(&mut self, age: u8) -> &mut Self {
self.age = Some(age);
self
}
fn email(&mut self, email: &str) -> &mut Self {
self.email = Some(email.to_string());
self
}
fn build(&self) -> Result<User, String> {
let name = self.name.clone().ok_or("Имя обязательно")?;
let age = self.age.ok_or("Возраст обязателен")?;
let email = self.email.clone().ok_or("Email обязателен")?;
if age < 18 {
return Err("Возраст должен быть 18+".to_string());
}
Ok(User { name, age, email })
}
}
fn main() {
match UserBuilder::new()
.name("Иван")
.age(25)
.email("ivan@example.com")
.build()
{
Ok(user) => println!("Пользователь {} создан", user.name),
Err(e) => println!("Ошибка: {}", e),
}
}
Пример 3: Адаптер для разных форматов данных
// Старый формат — CSV
struct CsvData;
impl CsvData {
fn get_csv(&self) -> String {
"name,age
Алиса,30".to_string()
}
}
// Новый формат — JSON
trait JsonData {
fn get_json(&self) -> String;
}
struct CsvToJsonAdapter {
csv: CsvData,
}
impl CsvToJsonAdapter {
fn new(csv: CsvData) -> Self {
CsvToJsonAdapter { csv }
}
}
impl JsonData for CsvToJsonAdapter {
fn get_json(&self) -> String {
let csv = self.csv.get_csv();
let lines: Vec<&str> = csv.split('
').collect();
let headers: Vec<&str> = lines[0].split(',').collect();
let values: Vec<&str> = lines[1].split(',').collect();
let mut json = String::from("{");
for i in 0..headers.len() {
json.push_str(&format!(""{}": "{}"", headers[i], values[i]));
if i < headers.len() - 1 {
json.push(',');
}
}
json.push('}');
json
}
}
fn main() {
let csv = CsvData;
let adapter = CsvToJsonAdapter::new(csv);
println!("JSON: {}", adapter.get_json());
}
Пример 4: Адаптер для разных версий API
// Старое API — возвращает строку
struct OldApi;
impl OldApi {
fn get_data_old(&self) -> String {
"старые данные".to_string()
}
}
// Новое API — возвращает число
trait NewApi {
fn get_data_new(&self) -> i32;
}
struct ApiAdapter {
old: OldApi,
}
impl ApiAdapter {
fn new(old: OldApi) -> Self {
ApiAdapter { old }
}
}
impl NewApi for ApiAdapter {
fn get_data_new(&self) -> i32 {
let data = self.old.get_data_old();
// Преобразуем строку в число (например, хеш)
data.len() as i32
}
}
fn main() {
let old = OldApi;
let adapter = ApiAdapter::new(old);
println!("Новые данные: {}", adapter.get_data_new());
}
Пример 5: Наблюдатель с несколькими типами событий
use std::rc::Rc;
use std::cell::RefCell;
trait EventListener {
fn on_event(&self, event_type: &str, data: &str);
}
struct EmailNotifier;
impl EventListener for EmailNotifier {
fn on_event(&self, event_type: &str, data: &str) {
println!("Email: событие '{}' с данными '{}'", event_type, data);
}
}
struct SmsNotifier;
impl EventListener for SmsNotifier {
fn on_event(&self, event_type: &str, data: &str) {
println!("SMS: событие '{}' с данными '{}'", event_type, data);
}
}
struct EventManager {
listeners: Vec<(String, Rc<RefCell<dyn EventListener>>)>,
}
impl EventManager {
fn new() -> Self {
EventManager { listeners: Vec::new() }
}
fn subscribe(&mut self, event_type: &str, listener: Rc<RefCell<dyn EventListener>>) {
self.listeners.push((event_type.to_string(), listener));
}
fn notify(&self, event_type: &str, data: &str) {
for (et, listener) in &self.listeners {
if et == event_type {
listener.borrow().on_event(event_type, data);
}
}
}
}
fn main() {
let mut manager = EventManager::new();
let email = Rc::new(RefCell::new(EmailNotifier));
let sms = Rc::new(RefCell::new(SmsNotifier));
manager.subscribe("login", email.clone());
manager.subscribe("login", sms.clone());
manager.subscribe("logout", email.clone());
manager.notify("login", "пользователь Иван");
manager.notify("logout", "пользователь Иван");
}
Пример 6: Комбинация Строителя и Наблюдателя
use std::rc::Rc;
use std::cell::RefCell;
trait Observer {
fn update(&self, message: &str);
}
struct Logger;
impl Observer for Logger {
fn update(&self, message: &str) {
println!("Лог: {}", message);
}
}
struct Config {
host: String,
port: u16,
observers: Vec<Rc<RefCell<dyn Observer>>>,
}
struct ConfigBuilder {
host: Option<String>,
port: Option<u16>,
observers: Vec<Rc<RefCell<dyn Observer>>>,
}
impl ConfigBuilder {
fn new() -> Self {
ConfigBuilder {
host: None,
port: None,
observers: Vec::new(),
}
}
fn host(&mut self, host: &str) -> &mut Self {
self.host = Some(host.to_string());
self
}
fn port(&mut self, port: u16) -> &mut Self {
self.port = Some(port);
self
}
fn add_observer(&mut self, observer: Rc<RefCell<dyn Observer>>) -> &mut Self {
self.observers.push(observer);
self
}
fn build(&self) -> Result<Config, String> {
let host = self.host.clone().ok_or("Host обязателен")?;
let port = self.port.ok_or("Port обязателен")?;
Ok(Config {
host,
port,
observers: self.observers.clone(),
})
}
}
impl Config {
fn notify(&self, message: &str) {
for obs in &self.observers {
obs.borrow().update(message);
}
}
}
fn main() {
let logger = Rc::new(RefCell::new(Logger));
let config = ConfigBuilder::new()
.host("127.0.0.1")
.port(8080)
.add_observer(logger)
.build()
.unwrap();
config.notify("Конфигурация загружена");
}
Пример 7: Адаптер для работы с разными базами данных
// Старая база данных — SQL
struct OldDatabase;
impl OldDatabase {
fn query_sql(&self, sql: &str) -> String {
format!("Результат SQL: {}", sql)
}
}
// Новая база данных — NoSQL
trait NewDatabase {
fn query_nosql(&self, query: &str) -> String;
}
struct DatabaseAdapter {
old_db: OldDatabase,
}
impl DatabaseAdapter {
fn new(old_db: OldDatabase) -> Self {
DatabaseAdapter { old_db }
}
}
impl NewDatabase for DatabaseAdapter {
fn query_nosql(&self, query: &str) -> String {
// Преобразуем NoSQL-запрос в SQL
let sql = format!("SELECT * FROM data WHERE key = '{}'", query);
self.old_db.query_sql(&sql)
}
}
fn main() {
let old = OldDatabase;
let adapter = DatabaseAdapter::new(old);
println!("{}", adapter.query_nosql("user:123"));
}
Сравнение с Python: чем похоже и чем отличается
| Аспект | Rust | Python |
|---|---|---|
| Динамическая типизация | Нет. Всё строго: типы проверяются на этапе компиляции. | Да. Можно передать любой объект, ошибка возникнет только в рантайме. |
| Владение и заимствование | Есть. Это усложняет реализацию Наблюдателя (нужны Rc, RefCell). | Нет. Просто сборщик мусора. Наблюдатель делается в 5 строк. |
| Строитель | Часто реализуется через структуру с Option. Возвращает Result. | Можно через класс с методами, возвращающими self. Или через **kwargs. |
| Адаптер | Через трейты. Нужно явно реализовать интерфейс. | Через утиную типизацию. Если объект имеет нужный метод — он подходит. |
| Производительность | Высокая. Нет накладных расходов на сборщик мусора. | Ниже. Но для большинства задач это не критично. |
| Сложность кода | Выше. Много boilerplate-кода (шаблонного). | Ниже. Код короче и проще для чтения. |
Совет: Если ты новичок — начни с Python, чтобы понять суть паттернов. Потом переходи на Rust, чтобы научиться писать надёжный и быстрый код.
Типичные ошибки и подводные камни
- Забыть вернуть
&mut Selfв методах Строителя. Если метод возвращает(), цепочка вызовов не сработает. Всегда возвращайself. - Использовать
clone()без необходимости. В Rust клонирование — дорогая операция. Старайся использовать ссылки, где возможно. - Путать
RcиArc.Rc— для одного потока,Arc— для многопоточности. В Наблюдателе чаще нуженRc. - Не обрабатывать
Resultвbuild(). Если Строитель возвращаетResult, всегда вызывайunwrap()или обрабатывай ошибку. - Создавать слишком много адаптеров. Если у тебя 10 адаптеров для 10 классов — возможно, стоит переделать архитектуру.
- Забывать отписывать наблюдателей. В Rust это не так критично (память освободится), но в больших системах может привести к утечкам.
- Использовать паттерны везде. Не надо делать Строитель для объекта с двумя полями. Паттерны нужны для сложных случаев.
Лучшие практики и советы профессионалов
- Начинай с простого. Если можно обойтись без паттерна — обойдись. Паттерн — это решение проблемы, а не украшение кода.
- Используй
cargo clippy. Это линтер для Rust. Он подскажет, если ты используешь паттерн неправильно. - Документируй паттерны. Если ты используешь Строитель, напиши комментарий: «Это Строитель для Pizza, потому что у неё много опциональных полей».
- Не бойся
RcиRefCell. Они нужны для Наблюдателя. Но помни:RefCellпроверяет заимствования в рантайме — если ошибёшься, программа упадёт. - Тестируй паттерны. Напиши unit-тесты для каждого метода. В Rust это просто: добавь
#[cfg(test)]и#[test]. - Изучай исходники популярных крейтов. Например,
serdeиспользует Строитель для десериализации. Посмотри, как это сделано профессионалами.
Вопросы для самопроверки
- Что такое паттерн проектирования?
Ответ: Это проверенный шаблон решения типовой задачи в программировании. Не готовый код, а рецепт. - Какие три группы паттернов существуют?
Ответ: Порождающие (создание объектов), структурные (сборка объектов), поведенческие (взаимодействие объектов). - Для чего нужен Строитель?
Ответ: Чтобы создавать сложные объекты пошагово, избегая огромных конструкторов. - Какой тип возвращают методы Строителя в Rust?
Ответ:&mut Self, чтобы можно было вызывать методы цепочкой. - Что делает Адаптер?
Ответ: Позволяет объектам с несовместимыми интерфейсами работать вместе, как переходник для розетки. - Как реализовать Адаптер в Rust?
Ответ: Создать структуру, которая хранит старый объект, и реализовать для неё новый трейт. - В чём сложность реализации Наблюдателя в Rust?
Ответ: Из-за системы владения нужно использоватьRc<RefCell<dyn Observer>>для хранения подписчиков. - Чем отличается
RcотArc?
Ответ:Rc— для одного потока,Arc— для многопоточности (атомарный счётчик ссылок). - Когда НЕ стоит использовать паттерн?
Ответ: Когда задача простая и решается без паттерна. Не усложняй код без необходимости. - Какой паттерн выбрать, если нужно уведомлять много объектов об изменениях?
Ответ: Наблюдатель (Observer).
Итог и что изучить дальше
Мы разобрали три фундаментальных паттерна: Строитель, Адаптер и Наблюдатель. Ты узнал:
- Как они работают на уровне теории и кода.
- Как реализовать их в Rust с учётом его особенностей.
- Какие ошибки допускают новички и как их избежать.
Эти паттерны — база. Если ты их освоил, ты сможешь читать и понимать чужой код, а также писать свой так, чтобы его было легко поддерживать.
Что изучать дальше?
- Фабричный метод (Factory Method) — ещё один порождающий паттерн. Позволяет создавать объекты, не указывая