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:
It can be hard to remember the order of the parameters.
It can be hard to know which parameters are optional and which are required.
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:
A Builder interface that defines the steps for constructing the object.
A ConcreteBuilder struct that implements the Builder interface and provides a way to construct the object.
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
- “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm