DEV Community

Hamp Goodwin
Hamp Goodwin

Posted on • Edited on

Instantiate → Initialize → Open over Functional Arguments

In this article I will describe the Functional Arguments constructor pattern, disagree with it, and suggest my preferred methods of construction, Struct Literal and Instantiate, 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()
}

Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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: ""}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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(),
    ...,
)
Enter fullscreen mode Exit fullscreen mode

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

https://twitter.com/rakyll/status/1000128803153170432

Top comments (0)