In this article I will describe the
Functional Arguments
constructor pattern, disagree with it, and suggest my preferred methods of construction,Struct Literal
andInstantiate, Initialize, Open
.
Functional Arguments
A very popular and widely adopted pattern for creating an object is the functional arguments pattern. This pattern in go is frequently attributed to Rob Pike, Dave Cheney, and further developed here. It is even mentioned as “good” in the uber-go style guide which is yet more complex but has testing benefits.
This pattern looks like this:
func NewObject(required string, options ...Option) (*Object, error) {
obj := &Object{}
// this could be and exported or not field
obj.property = "sane default"
for _, opt := range options {
if err := opt(o); err != nil {
return nil, err
}
}
return obj, nil
}
type Option func(*Object) error
func WithProperty(p string) Option {
return func(o *Object) error {
o.property = p
return nil
}
}
//... and many more functions, however many you need.
// ---
// main.go
func main() {
obj, err := NewObject("required", WithProperty("string"))
if err != nil {
log.Panicf(err)
}
obj.Open()
}
This pattern claims some advantages over other ways of constructing object.
- API’s that are beautiful and can grow over time
- default use case to be simplest
- more readable, meaningful parameters
- provide direct control over the initialization of complex values
I disagree
I don’t think any of these are more true over other patterns. I believe the appeal of functional arguments is vanity. They look good, are fun and exciting to use when you understand their indirection. However, Go is beautiful because it’s boring. The excitement of this pattern holds no benefit over out of the box features in Go. More discussion can be found in this old twitter thread from Jaana Dogan.
Design patterns like functional arguments for constructors often appear because the language in which they exist is missing a feature to fulfill what the design pattern fulfills. I think this pattern is attempting to fill a perceived missing feature of Go.
See “Are Design Patterns Missing Language Features” and “Design Patterns are Missing Language Features” to help form your own opinion.
In this case, I disagree that Go is missing features to easily solve this problem.
API’s that are beautiful and can grow over time
Beauty to me is readability, ease of discovery, and a balance of simplicity and complexity. The pattern is only readable to those who know the pattern. Discovering the optional parameters is not easy. Functional options grows just as elegantly as other constructor patterns; whichever way you look at it you add, modify, or remove another “something” and then implement it.
default use case to be the simplest
I agree that
o := NewObject("required")
// or
o = NewObject("required", WithFoo(), WithBar())
appears more simple than:
// required is a required string
o := NewObject("required")
// or
o := NewObject("required")
o.Foo = "foo"
o.Bar = "bar"
// or for unexported
// o.SetFoo("foo")
// o.SetBar("bar")
However, to me it is not more simple. I do not see how functional options improves the implementation of default values making it simpler.
more readable, meaningful parameters
The pattern of constructing an object with functions whose names follow a convention like WithXYZ()
is not a more readable nor meaningful.
o := NewObject("required", WithXYZ())
// vs
o := NewObject("required")
o.XYZ = val
What matters is good naming conventions, consistency, and documentation blocks.
provide direct control over the initialization of complex values
Again, a functional arguments pattern provides no more control over initialization of complex values than say a method to configure an object.
o := NewObject("required", WithComplexConfiguration())
// vs
o := NewObject("required")
o.ComplexConfiguration()
What I prefer
I prefer two ways of constructing objects. The first is the simplest, most common, and easiest to read. Creating a struct literal.
o := &Object{Property: ""}
I hope this looks familiar. In fact those who work with http web servers in go use this a lot.
srv := &http.Server{} // put whatever in here
src.ListenAndServe()
When we need a more complex construction with default values, we can use a Constructor function following Instantiate, Initialize and Open.
// instantiate
s := NewServer()
// initialize
s.Addr = ":6060"
s.ClientTimeout = 30 * time.Second
s.MaxConns = 5
s.MaxConcurrent = 10
s.Cert = myCert
// open
if err := s.Open(); err != nil {
return err
}
If you again have more complex configuration, need to set unexported fields, or often need a non default specially configured server, the following works.
// instantiate
s := NewServer()
// initialize
s.SetDefaults()
// or
s.SetSpecial()
s.SetUnexportedField()
// open
if err := s.Open(); err != nil {
return err
}
But what if we have a lot of required values for construction? Doesn’t it become complex and annoying to maintain?
s := NewServer(
"name",
":1234",
5,
...,
)
s.OptionalField = "tls"
// vs
s := NewServer(
WithName("name"),
WithPort(":1234"),
WithMaxRetry(5"),
WithTLS(),
...,
)
No. GoDoc, autocompletion, intellisense, and Go IDE plugins all indicate what the values are in the standard constructor; which is also the reason I don’t use configuration structs.
Summary
I have implemented all of the above many times. I regret the number of times I implemented the functional arguments constructor pattern, and configuration struct patterns. There are more criticisms of the pattern which I’ve held. The amount of explanation that it took to demonstrate and disagree with the pattern is a testament to its complexity. Counterpoint to its complexity are the two self evident simple examples provided.
links
https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
https://sagikazarmark.hu/blog/functional-options-on-steroids/
https://github.com/uber-go/guide/blob/master/style.md#functional-options
https://wiki.c2.com/?AreDesignPatternsMissingLanguageFeatures
https://wiki.c2.com/?DesignPatternsAreMissingLanguageFeatures
Top comments (0)