Перейти к содержанию

Шаблон Builder в Rust - универсальное решение

Posted on:14 апреля 2023 г. at 08:51

Example Dynamic OG Image link

Вступление

В предыдущей статье я описал шаблон Builder в Rust. Однако, несмотря на то, что решение сработало, выглядело оно не очень элегантно.

Следовательно, я нашел более элегантное решение, которое представлю здесь.

Давайте напомним себе, как выглядит шаблон Builder:

Давайте разберем это на части:

  1. Director это клиентский класс для Builder, и он запрашивает создание некоторого продукта.
  2. Интерфейс конструктора. Это универсальный интерфейс для любого конструктора, и он содержит методы для создания продукта.
  3. ConcreteBuilder это класс, в котором создается продукт. Поскольку мы можем использовать только интерфейс, ConcreteBuilders можно изменять местами для создания различных продуктов.
  4. Продукт, который мы хотим создать, - это класс Product. Это также могло бы определить интерфейс.

Реализация с использованием дженериков

Откройте свой терминал или командную строку в пустом каталоге и введите:

cargo new rust_builder_with_generics
cd rust_builder_with_generics

Мы начнем с определения структуры Bicycle:

#[derive(Clone,Debug)]
pub struct Bicycle {
  number_of_wheels: i8,
  bike_type: String
}

impl Bicycle {
  fn new()->Self {
    Bicycle {
      number_of_wheels: 0,
      bike_type: "".to_string()
    }
  }
}

У велосипеда есть количество колес и bike_type. Кроме того, мы определяем простой конструктор.

BicycleBuilder определяется как трейт:

pub trait BicycleBuilder {
  fn add_wheels(&mut self)->&mut dyn BicycleBuilder;
  fn set_type(&mut self)->&mut dyn BicycleBuilder;
  fn build(&self) -> Bicycle;
}

В этом параметре мы используем методы add_wheels, задаем тип и создаем готовый продукт.

Далее мы определим два типа велосипедов, сначала ATBBuilder:

struct ATBBuilder {
  bicycle: Bicycle
}

impl BicycleBuilder for ATBBuilder {
  fn add_wheels(&mut self) -> &mut dyn BicycleBuilder {
    self.bicycle.number_of_wheels=2;
    self
}

  fn set_type(&mut self) -> &mut dyn BicycleBuilder {
    self.bicycle.bike_type="ATB".to_string();
    self
  }

  fn build(&self)->Bicycle {
    self.bicycle.clone()
  }
}

impl ATBBuilder {
  fn new()-> Self {
    Self {
      bicycle: Bicycle::new()
    }
  }
}

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

StreetBikeBuilder похож на ATBBike:

struct StreetBikeBuilder {
  bicycle:Bicycle
}

impl BicycleBuilder for StreetBikeBuilder {
  fn add_wheels(&mut self) -> &mut dyn BicycleBuilder {
    self.bicycle.number_of_wheels=3;
    self
  }

  fn set_type(&mut self) -> &mut dyn BicycleBuilder {
    self.bicycle.bike_type="Street".to_string();
    self
}

fn build(&self)->Bicycle {
  self.bicycle.clone()
 }
}

impl StreetBikeBuilder {
  fn new()->Self {
    Self {
      bicycle:Bicycle::new()
    }
  }
}

Опять же, все говорит само за себя, также добавлен конструктор.

Теперь мы переходим к BikeEngineer, который мы будем реализовывать с помощью дженериков:

struct BikeEngineer<T:BicycleBuilder> {
  builder: T
}

impl<T:BicycleBuilder> BikeEngineer<T> {
  fn new(builder: T)->Self {
    BikeEngineer {
      builder: builder
    }
  }

  fn construct_bike(&mut self)->Bicycle {
    self.builder
      .add_wheels()
      .set_type()
      .build()
  }
}

Краткое пояснение:

  1. BikeEngineer получает универсальный параметр T с одним ограничением: T должен реализовывать характеристику BicycleBuilder.
  2. Таким образом, поле builder имеет тип T.
  3. Один и тот же общий параметр и ограничения передаются реализации.
  4. Поскольку мы знаем, что builder - это тип, который реализует интерфейс BicycleBuilder, метод construct не претерпевает изменений.

Давайте проверим это:

fn main() {
  let builder=StreetBikeBuilder::new();
  let mut engineer=BikeEngineer::new(builder);
  let bike=engineer.construct_bike();
  println!("{:?}",bike);
}

Строка за строкой:

  1. Сначала мы создаем экземпляр StreetBikeBuilder.
  2. Затем мы передаем это конструктору BikeEngineer. Обратите внимание, как Rust определяет общий тип параметра.
  3. Далее мы конструируем велосипед и распечатываем его.

Вывод

Как вы можете видеть, использование дженериков дает нам элегантное решение этой проблемы и, вероятно, является более идиоматичным, то есть это правильный Rust.

Однако будьте осторожны, даже если это работает для этой небольшой и упрощенной задачи, это может не сработать для других задач.