An easy way to design extensible constructors that accept an arbitrary number of arguments
The options pattern is a way to design extensible constructors that accept an arbitrary number of arguments. Let’s get started with this code:
type person struct {
age int
hairColour string
}
// the constructor using the "option" function
func NewPerson(opts ...PersonOption) *person {
p := new(person)
for _, opt := range opts {
opt(p) // calls the option
}
}
// the "option" function
type PersonOption func(p *person)
func withHairColour(colour string) PersonOption {
return func(p *person) {
p.hairColour = colour
}
}
func withAge(age int) PersonOption {
return func(p *person) {
p.age = age
}
}
func main() {
person := NewPerson(withHairColour("light brown"), withAge(30))
// person.hairColour is now light brown
// person.age is now 30
}
Its main building block is the option function (type PersonOption func(p *person), which accepts a single argument, namely the value to modify (p *person). The functions withHairColour and withAge are both higher-order functions (HOFs) that return the PersonOption function.
It’s also possible to implement the options pattern using single-method interfaces (SMIs). The trick is to replace the option function with an SMI. This leads to a choice: do you want to use HOFs such as withHairColour or define types that implement the option SMI?
We want a flexible function for building responses in an HTTP controller. And we want to use HOFs to achieve this. The following example demonstrates how to achieve this:
// the "option" SMI
type responseOption interface {
apply(w http.ResponseWriter)
}
// an adapter type similar to http.HandlerFunc whose single purpose
// is to allow functions to be used as the responseOption interface
type responseOptionFunc func(w http.ResponseWriter)
func (fn responseOptionFunc) apply(w http.ResponseWriter) {
fn(w)
}
// HOF that sets the response's HTTP status code
func withStatusCode(code int) responseOption {
return responseOptionFunc(func(w http.ResponseWriter){
w.WriteHeader(code)
})
}
// HOF that marshalls a JSON object into bytes and writes them to the output.
// The HOF also sets the "content-type" to an appropriate value for a JSON
// payload.
func withJSON(obj any) responseOption {
return responseOptionFunc(func(w http.ResponseWriter){
w.Header().Set("Content-Type", "application/json")
b, _:= json.Marshal(obj)
w.Write(b)
})
}
// The API we want to achieve. A flexible function that can respond
// in any way depending only on the passed opts.
func respond(w http.ResponseWriter, opts ...responseOption) {
for _, opt := range opts {
opt.apply(w)
}
}
// controller method usage example
func (ctlr *controller) listResource(w http.ResponseWriter, r *http.Request) {
// implementation omitted for brevity
respond(withStatusCode(200), withJSON(obj))
}
Another option is to omit defining the adapter type and not use HOFs at all. This is presented below:
// the "option" SMI
type responseOption interface {
apply(w http.ResponseWriter)
}
// instead of a HOF we use a type that implements the "option" SMI.
type withStatusCode int
func (c withStatusCode) apply(w http.ResponseWriter) {
w.WriteHeader(c)
}
type withJSON struct {
obj any
}
func (json withJSON) apply(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
b, _:= json.Marshal(json.obj)
w.Write(b)
}
// The API we want to achieve. A flexible function that can respond
// in any way depending only on the passed opts.
func respond(w http.ResponseWriter, opts ...responseOption) {
for _, opt := range opts {
opt.apply(w)
}
}
// controller method usage example
func (ctlr *controller) listResource(w http.ResponseWriter, r *http.Request) {
// implementation omitted for brevity
respond(withStatusCode(200), withJSON{obj})
}
Conclusion
It’s possible to use SMIs to implement the options pattern instead of the purely functional approach. In most cases, which to choose is a matter of personal preference. Using SMIs results in slightly more verbose code but allows for more freedom since the “option” may be any data structure that fulfills the option interface. On the other hand, if a function is all you need, that’s what you should use for the option definition.
The Options Pattern Using Single-Method Interfaces was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.