Skip to content

Latest commit

 

History

History
193 lines (149 loc) · 9.41 KB

File metadata and controls

193 lines (149 loc) · 9.41 KB

Глава 12. Идиомы и Паттерны проектирования в Rust

1. Паттерн Builder (Строитель)

В некоторых языках есть "необязательные аргументы" в функциях или перегрузка методов. В Rust этого нет. Если структура состоит из 10 полей, и большинство из них имеют стандартные значения, создавать её напрямую через Struct { ... } очень больно — придется прописывать каждое поле.

Для этого используется паттерн Builder. Мы создаем отдельную структуру, которая пошагово собирает наш объект.

Представь, что мы настраиваем веб-сервер.

// 1. Финальная структура сервера (которую мы хотим получить)
#[derive(Debug)]
struct Server {
    host: String,
    port: u16,
    tls_enabled: bool,
}

// 2. Структура-строитель
struct ServerBuilder {
    host: String,
    port: u16,
    tls_enabled: bool,
}

impl ServerBuilder {
    // Начальное состояние
    fn new(host: &str) -> Self {
        ServerBuilder {
            host: host.to_string(),
            port: 80,             // Стандартный порт
            tls_enabled: false,   // Без шифрования по умолчанию
        }
    }

    // Методы-модификаторы.
    // Обрати внимание: они принимают `mut self` (забирают владение строителем), 
    // меняют его и возвращают обратно `Self`. Это позволяет выстраивать цепочку!
    fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }

    fn tls(mut self, enabled: bool) -> Self {
        self.tls_enabled = enabled;
        self
    }

    // Финальный метод, который превращает Строителя в готовый Сервер
    fn build(self) -> Server {
        Server {
            host: self.host,
            port: self.port,
            tls_enabled: self.tls_enabled,
        }
    }
}

fn main() {
    // Магия! Мы собираем сервер цепочкой вызовов (method chaining).
    let my_server = ServerBuilder::new("127.0.0.1")
        .port(8080)
        .tls(true)
        .build();

    println!("Сервер запущен: {:#?}", my_server);
}

Этот подход невероятно популярен в экосистеме Rust. Если ты будешь использовать любую крупную библиотеку (например, для создания HTTP-запросов), ты столкнешься с Builder'ом.

2. Паттерн Typestate (Машина состояний на типах)

Это самая уникальная и мощная фишка Rust. В классическом программировании, если у тебя есть документ, он может быть "Черновиком", "На проверке" и "Опубликованным". Обычно это делают так: создают одно поле state (состояние) внутри структуры. И в каждом методе (например, publish()) пишут if state == "Черновик" { ОШИБКА }.

Проблема в том, что эта ошибка всплывет только во время работы программы (в рантайме).

В Rust мы используем систему типов и Владение, чтобы сделать неверные действия физически невозможными на этапе компиляции! Суть в том, что каждое состояние — это отдельная структура.

Давай создадим систему публикации постов:

// 1. Создаем три разные структуры для каждого состояния
struct DraftPost {
    content: String,
}

struct PendingReviewPost {
    content: String,
}

struct PublishedPost {
    content: String,
}

// 2. Метод создания всегда возвращает Черновик
impl DraftPost {
    fn new(text: &str) -> DraftPost {
        DraftPost { content: text.to_string() }
    }

    // МАГИЯ ЗДЕСЬ: Метод забирает владение `self` (Черновиком)
    // и возвращает НОВЫЙ тип (Пост на проверке).
    // Старый черновик уничтожается!
    fn request_review(self) -> PendingReviewPost {
        PendingReviewPost { content: self.content }
    }
}

impl PendingReviewPost {
    // Одобряем пост. Забираем Пост на проверке, выдаем Опубликованный.
    fn approve(self) -> PublishedPost {
        PublishedPost { content: self.content }
    }
    
    // Если отклоняем - возвращаем обратно Черновик
    fn reject(self) -> DraftPost {
        DraftPost { content: self.content }
    }
}

impl PublishedPost {
    // И только Опубликованный пост имеет метод "прочитать"
    fn read(&self) -> &str {
        &self.content
    }
}

fn main() {
    let my_draft = DraftPost::new("Изучаем Typestate");

    // my_draft.read(); 
    // ОШИБКА КОМПИЛЯЦИИ! У Черновика нет метода read(). 
    // Мы физически не можем прочитать неопубликованный пост!

    let review_post = my_draft.request_review(); 
    // my_draft больше не существует! Мы передали Владение (move). 
    // Мы не можем случайно запросить ревью дважды.

    let published = review_post.approve();

    // А вот теперь можно читать
    println!("Пост в эфире: {}", published.read());
}

Почему это так круто? Компилятор Rust выступает гарантом нашей бизнес-логики. Мы не можем "выстрелить из незаряженного ружья", потому что у типа UnloadedGun просто нет метода fire(). Метод fire() есть только у типа LoadedGun. Это паттерн делает код железобетонным.

3. Паттерн Newtype (Новый тип)

В базах данных идентификаторы пользователя, заказа и товара — это часто просто большие числа (например, i64). Представь такую функцию:

fn pay_for_order(user_id: i64, order_id: i64) {
    // логика оплаты
}

Если программист случайно вызовет её так: pay_for_order(order_id, user_id), компилятор промолчит (ведь это просто два числа i64). Программа спишет деньги не с того человека!

Чтобы избежать этой катастрофы, в Rust используется паттерн Newtype. Мы оборачиваем базовый тип в "структуру-кортеж с одним элементом".

// Создаем новые уникальные типы
struct UserId(i64);
struct OrderId(i64);

// Теперь компилятор строго следит за тем, что мы передаем!
fn secure_pay(user: UserId, order: OrderId) {
    println!("Оплата заказа {} пользователем {}", order.0, user.0); // Доступ через .0
}

fn main() {
    let uid = UserId(100500);
    let oid = OrderId(999);

    // secure_pay(oid, uid); 
    // ОШИБКА КОМПИЛЯЦИИ! Компилятор скажет: "Ожидался UserId, а получен OrderId".
    
    secure_pay(uid, oid); // Работает идеально
}

Бонус производительности: Точно так же, как и с дженериками, эта обертка существует только на этапе компиляции. Когда программа будет собрана, в памяти это будут просто "голые" числа i64. Защита абсолютно бесплатна.


Резюме:

  • Builder помогает пошагово и элегантно создавать структуры с кучей параметров.
  • Typestate использует систему типов и правила Владения, чтобы сделать неверную бизнес-логику невозможной для компиляции. Состояние — это тип. Смена состояния — это уничтожение старого типа и возврат нового.
  • Newtype оборачивает простые типы (числа, строки) в уникальные структуры, чтобы компилятор бил по рукам, если мы перепутаем ID пользователя с ID заказа.