Rust builder mode (Builder)

Builder mode (Builder)

summary

Builder pattern is a design pattern that provides a flexible solution and has solved various object creation problems in object-oriented programming. The purpose of builder design pattern is to separate the construction of complex objects from their representation. It is one of the "Gang of four" design patterns [wiki]. Builder pattern is a creative design pattern that enables you to create complex objects step by step. This mode allows you to generate different types and forms of objects using the same creation code.

Definition: the purpose of Builder design pattern is to separate the construction of complex objects from their representation. By doing so, the same construction process can create different representations.

history

If there is a complex object that needs to be constructed, many member variables and nested objects need to be initialized. Sometimes these initialization codes are usually hidden in an incomprehensible constructor with many parameters; Or these codes are scattered in multiple locations of the client code.

  1. For example, create a house, different kinds of houses have different styles, and create a subclass for each type of house, which may make the program too complex.
  2. Or you don't need to generate subclasses, but you need to create a super constructor that includes all possible parameters and use it to control the creation of house objects. Although this can avoid generating subclasses, it will cause the constructor with a large number of input parameters not to be used all the time. Usually, most of the parameters are not used, which makes the call to the constructor very concise.

Use of builder mode

The builder pattern recommends that the code constructed by the object be extracted from the product class and placed in a separate object called a generator. Each time you create an object, you need to perform a series of steps through the generator object. The point is that you don't need to call all the steps, but only those required to create a specific object configuration.

Applicable scenario

  • Using the builder design pattern can avoid the appearance of "overlapping constructors".
    • Assuming that there are more than a dozen optional parameters in a complex function, it will be very inconvenient to call these functions. Therefore, you need to overload this constructor and create several simplified versions with fewer parameters.
    • The builder design pattern allows you to generate objects in steps and allows you to apply only the necessary steps.
  • When using code to create different forms of products, you can use the generator pattern
    • If you need to create various forms of products with similar manufacturing processes and only differences in details, you can use the generator mode.
    • All possible manufacturing steps are defined in the basic generator interface, and the specific generator will implement these steps to manufacture specific forms of products.
  • Use constructor mode to construct other complex objects
    • The constructor pattern allows you to build a product step by step. You can delay some steps without affecting the final product.

advantage

  • You can create objects step by step, suspend the creation step, or run the creation step recursively.
  • To generate different forms of products, you can reuse the same manufacturing code
  • The principle of single responsibility can separate the complex construction code from the business logic of the product.

shortcoming

Because this pattern needs to add multiple classes, the overall complexity of the code will increase.

describe

Create an object by using the builder assistant.

example

fn main() {
    let foo = Foo {
        bar: String::from("Y"),
    };
    let foo_from_builder = FooBuilder::new().name(String::from("Y")).build();
    println!("foo = {:?}", foo);
    println!("foo from builfer = {:?}", foo_from_builder);
}

#[derive(Debug, PartialEq)]
pub struct Foo {
    // lots of complicated fields
    bar : String,
}

pub struct FooBuilder {
    // Probably lots of optional fields.
    bar: String,
}

impl FooBuilder {
    pub fn new() -> Self {
        // set the minimally required fields of Foo.
        Self {
            bar: String::from("x"),
        }
    }

    pub fn name(mut self, bar: String) -> FooBuilder {
        // set the name on the builder iteself,
        // and return the builder by value.
        self.bar = bar;
        self 
    }
    // if we can get away with not consuming the builder here, that is an 
    // advantage. It means we can use the FooBuilder as a template for constructing many Foo.
    pub fn build(self) -> Foo {
        // Create a Foo from Foo the FooBuilder, applying all settings in FooBuilder to Foo. 
        Foo { bar: self.bar }
    }
}
// The way of Rust programming P234
struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

struct CircleBuilder {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
    fn new() -> CircleBuilder {
        CircleBuilder {
            x: 0.0, y: 0.0, radius: 1.0,
        }
    }
}

impl CircleBuilder {
    fn x(&mut self, coordinate: f64) -> &mut CircleBuilder {
        self.x = coordinate;
        self
    }
    fn y(&mut self, coordinate: f64) -> &mut CircleBuilder {
        self.y = coordinate;
        self
    }
    fn radius(&mut self, radius: f64) -> &mut CircleBuilder {
        self.radius = radius;
        self
    }

    fn build(&self) -> Circle {
        Circle {
            x: self.x, y: self.y, radius: self.radius,
        }
    }
}

fn main() {
  let c = Circle::new().x(1.0).y(2.0).radius(2.0).build();
  println!("area = {:?}", c.area());
  println!("c.x = {:?}", c.x);
  println!("c.y = {:?}", c.y);
}

motivation

This method is useful when you need many different constructors or when construction has side effects.

advantage

Separate the construction method from other methods.

Prevent proliferation of constructors

It can be used for single initialization and more complex construction.

shortcoming

It is more complex than directly creating structure objects or simple constructors.

discuss

This pattern is more common in Rust (and simple objects) than in many other languages because Rust lacks overloading. Since you can only use a single method with a given name, using multiple constructors in Rust is better than C + +, Java or other languages.

This pattern is often used where the builder object itself is useful, not just a builder. For example: std::process::Command is the builder of Child. In this case, the naming patterns of T and TBuilder are not used.

This example gets and returns the generator by value. Accepting and returning the builder as a mutable reference is generally more ergonomic (and more efficient).

let mut fb = FooBuilder::new();
fb.a();
fb.b();
let f = fb.builder();

And foobuilder:: new() a(). b(). Builder () style.

See

Keywords: Rust

Added by furious5 on Mon, 07 Mar 2022 22:30:41 +0200