🦀 Rust

أنماط التصميم في 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, чтобы научиться писать надёжный и быстрый код.

Типичные ошибки и подводные камни

  1. Забыть вернуть &mut Self в методах Строителя. Если метод возвращает (), цепочка вызовов не сработает. Всегда возвращай self.
  2. Использовать clone() без необходимости. В Rust клонирование — дорогая операция. Старайся использовать ссылки, где возможно.
  3. Путать Rc и Arc. Rc — для одного потока, Arc — для многопоточности. В Наблюдателе чаще нужен Rc.
  4. Не обрабатывать Result в build(). Если Строитель возвращает Result, всегда вызывай unwrap() или обрабатывай ошибку.
  5. Создавать слишком много адаптеров. Если у тебя 10 адаптеров для 10 классов — возможно, стоит переделать архитектуру.
  6. Забывать отписывать наблюдателей. В Rust это не так критично (память освободится), но в больших системах может привести к утечкам.
  7. Использовать паттерны везде. Не надо делать Строитель для объекта с двумя полями. Паттерны нужны для сложных случаев.

Лучшие практики и советы профессионалов

  • Начинай с простого. Если можно обойтись без паттерна — обойдись. Паттерн — это решение проблемы, а не украшение кода.
  • Используй cargo clippy. Это линтер для Rust. Он подскажет, если ты используешь паттерн неправильно.
  • Документируй паттерны. Если ты используешь Строитель, напиши комментарий: «Это Строитель для Pizza, потому что у неё много опциональных полей».
  • Не бойся Rc и RefCell. Они нужны для Наблюдателя. Но помни: RefCell проверяет заимствования в рантайме — если ошибёшься, программа упадёт.
  • Тестируй паттерны. Напиши unit-тесты для каждого метода. В Rust это просто: добавь #[cfg(test)] и #[test].
  • Изучай исходники популярных крейтов. Например, serde использует Строитель для десериализации. Посмотри, как это сделано профессионалами.

Вопросы для самопроверки

  1. Что такое паттерн проектирования?
    Ответ: Это проверенный шаблон решения типовой задачи в программировании. Не готовый код, а рецепт.
  2. Какие три группы паттернов существуют?
    Ответ: Порождающие (создание объектов), структурные (сборка объектов), поведенческие (взаимодействие объектов).
  3. Для чего нужен Строитель?
    Ответ: Чтобы создавать сложные объекты пошагово, избегая огромных конструкторов.
  4. Какой тип возвращают методы Строителя в Rust?
    Ответ: &mut Self, чтобы можно было вызывать методы цепочкой.
  5. Что делает Адаптер?
    Ответ: Позволяет объектам с несовместимыми интерфейсами работать вместе, как переходник для розетки.
  6. Как реализовать Адаптер в Rust?
    Ответ: Создать структуру, которая хранит старый объект, и реализовать для неё новый трейт.
  7. В чём сложность реализации Наблюдателя в Rust?
    Ответ: Из-за системы владения нужно использовать Rc<RefCell<dyn Observer>> для хранения подписчиков.
  8. Чем отличается Rc от Arc?
    Ответ: Rc — для одного потока, Arc — для многопоточности (атомарный счётчик ссылок).
  9. Когда НЕ стоит использовать паттерн?
    Ответ: Когда задача простая и решается без паттерна. Не усложняй код без необходимости.
  10. Какой паттерн выбрать, если нужно уведомлять много объектов об изменениях?
    Ответ: Наблюдатель (Observer).

Итог и что изучить дальше

Мы разобрали три фундаментальных паттерна: Строитель, Адаптер и Наблюдатель. Ты узнал:

  • Как они работают на уровне теории и кода.
  • Как реализовать их в Rust с учётом его особенностей.
  • Какие ошибки допускают новички и как их избежать.

Эти паттерны — база. Если ты их освоил, ты сможешь читать и понимать чужой код, а также писать свой так, чтобы его было легко поддерживать.

Что изучать дальше?

  • Фабричный метод (Factory Method) — ещё один порождающий паттерн. Позволяет создавать объекты, не указывая