Golang - Options vs Builder Pattern

Golang - Options vs Builder Pattern

When it comes to creating complex objects in Golang, two patterns are commonly used: the Options pattern and the Builder pattern. Both patterns have their advantages and disadvantages, and choosing the right pattern depends on the specific needs of your project.

The Problem

Let's say we want to create a complex object that has many optional parameters. One way to do this is to create a constructor that takes in all the parameters and provides default values for the optional ones. However, this approach has a few downsides:

  1. It can be hard to remember the order of the parameters.

  2. It can be hard to know which parameters are optional and which are required.

  3. The constructor can become very long and hard to read.


The Solution: Options Pattern

The Options pattern can be used to create objects with many optional parameters. In this pattern, we define a struct with optional parameters and provide methods to set those parameters. This pattern can be more concise than the Builder pattern and can be easier to use for objects with fewer parameters.

Example

In Golang, the Options pattern can be implemented using functional options. Functional options are functions that take a struct as an argument and return a modified version of that struct. Here's an example of how the Options pattern can be used to create a pizza object:

type Pizza struct {
    dough     string
    sauce     string
    cheese    string
    toppings  []string
}

type PizzaOptions struct {
    Dough     string
    Sauce     string
    Cheese    string
    Toppings  []string
}

type PizzaOption func(*PizzaOptions)

func WithDough(dough string) PizzaOption {
    return func(po *PizzaOptions) {
        po.Dough = dough
    }
}

func WithSauce(sauce string) PizzaOption {
    return func(po *PizzaOptions) {
        po.Sauce = sauce
    }
}

func WithCheese(cheese string) PizzaOption {
    return func(po *PizzaOptions) {
        po.Cheese = cheese
    }
}

func WithToppings(toppings []string) PizzaOption {
    return func(po *PizzaOptions) {
        po.Toppings = toppings
    }
}

func NewPizza(options ...PizzaOption) *Pizza {
    opts := &PizzaOptions{}
    for _, option := range options {
        option(opts)
    }

    pizza := &Pizza{
        dough: opts.Dough,
        sauce: opts.Sauce,
        cheese: opts.Cheese,
        toppings: opts.Toppings,
    }

    return pizza
}

In this example, we define the Pizza struct and the PizzaOptions struct, which is a struct with optional parameters. We then define functions to set each option, such as WithDough, WithSauce, and WithToppings. These functions return a PizzaOption that sets the corresponding field on the PizzaOptions struct. Finally, we define a NewPizza function that takes any number of PizzaOptions and constructs a Pizza object.

func main() {
    pizza := NewPizza(
        WithDough("Regular"),
        WithSauce("Tomato"),
        WithCheese("Mozzarella"),
        WithToppings([]string{"Pepperoni", "Olives", "Mushrooms"}),
    )

    println(pizza.dough)
    println(pizza.sauce)
    println(pizza.cheese)
    println(pizza.toppings)
}

The Options pattern can be a good alternative to the Builder pattern for creating objects with many optional parameters, especially if the object has fewer parameters. However, it can become unwieldy for objects with many parameters, as the number of functions needed to set all the options can become large.

Usage in the Golang stdlib

The Options pattern is used in the Golang standard library for creating objects such as the http.Request object, which has many optional parameters. The http.NewRequest function takes a method, URL, and optional headers and a body, among other parameters, and returns a new http.Request object. The headers and body are optional parameters that can be set using functional options.


The Alternative: Builder Pattern

The Builder pattern provides a solution to these problems by separating the construction of a complex object from its representation. The Builder pattern involves the following components:

  1. A Builder interface that defines the steps for constructing the object.

  2. A ConcreteBuilder struct that implements the Builder interface and provides a way to construct the object.

  3. A Director struct that uses the Builder to construct the object.

Example

Here's an example implementation of the Builder pattern in Golang, using the pizza object mentioned in the article:

type Pizza struct {
    dough     string
    sauce     string
    cheese    string
    toppings  []string
}

type PizzaBuilder interface {
    SetDough(string) PizzaBuilder
    SetSauce(string) PizzaBuilder
    SetCheese(string) PizzaBuilder
    SetToppings([]string) PizzaBuilder
    Build() *Pizza
}

type ConcretePizzaBuilder struct {
    pizza *Pizza
}

func NewConcretePizzaBuilder() *ConcretePizzaBuilder {
    return &ConcretePizzaBuilder{pizza: &Pizza{}}
}

func (cpb *ConcretePizzaBuilder) SetDough(dough string) PizzaBuilder {
    cpb.pizza.dough = dough
    return cpb
}

func (cpb *ConcretePizzaBuilder) SetSauce(sauce string) PizzaBuilder {
    cpb.pizza.sauce = sauce
    return cpb
}

func (cpb *ConcretePizzaBuilder) SetCheese(cheese string) PizzaBuilder {
    cpb.pizza.cheese = cheese
    return cpb
}

func (cpb *ConcretePizzaBuilder) SetToppings(toppings []string) PizzaBuilder {
    cpb.pizza.toppings = toppings
    return cpb
}

func (cpb *ConcretePizzaBuilder) Build() *Pizza {
    return cpb.pizza
}

type Director struct {
    builder PizzaBuilder
}

func NewDirector(builder PizzaBuilder) *Director {
    return &Director{builder: builder}
}

func (d *Director) Construct() *Pizza {
    return d.builder.SetDough("Thin Crust").SetSauce("Tomato").SetCheese("Mozzarella").SetToppings([]string{"Mushrooms", "Olives", "Onions"}).Build()
}

In this example, we define the Pizza struct and the PizzaBuilder interface. The ConcretePizzaBuilder struct implements the PizzaBuilder interface and provides a way to construct the Pizza object. The Director struct uses the PizzaBuilder to construct the Pizza object. The Director struct is not strictly necessary, but it provides a way to simplify the process of constructing the Pizza object.

We can use the Director and ConcretePizzaBuilder to create a Pizza object as follows:

builder := NewConcretePizzaBuilder()
director := NewDirector(builder)
pizza := director.Construct()

This creates a Pizza object with the following properties:

  • Dough: Thin Crust

  • Sauce: Tomato

  • Cheese: Mozzarella

  • Toppings: Mushrooms, Olives, Onions

Note that we only had to specify the properties that we wanted to change. All the other properties were set to default values. This makes it easier to create complex objects with many optional parameters, without having to remember the order of the parameters or which parameters are optional and which are required.

Usage in the Golang stdlib

The Builder pattern is not used in the Golang standard library, but it is a commonly used pattern in Golang applications to create complex objects with many optional parameters. The Options pattern is also used in Golang applications as an alternative to the Builder pattern for creating objects with many optional parameters.


Conclusion

The Options pattern is an alternative to the Builder pattern that can be used to create objects with many optional parameters. It can be more concise than the Builder pattern but can become unwieldy for objects with many parameters. In Golang, the Options pattern can be implemented using functional options.

The Builder pattern is a powerful pattern that allows you to create complex objects with many optional parameters. It separates the construction of the object from its representation and provides a way to create different representations of the same object using the same construction process. In Golang, the Builder pattern can be used to create complex objects with ease.


References

Did you find this article valuable?

Support Matthias Bruns by becoming a sponsor. Any amount is appreciated!