Design patterns constantly help us to design our software architecture, and allow programmers to speak in a common language that make easier the transmission of concepts regardless of the programming language used.
The most used are those that help us to initialize type values: Creational Patterns.
Recently I found a functional pattern that in my case fits most of the time:
Functional Options
.
Before define and look at examples of how to use this pattern, I want to
dive in how we normally initialize objects in Go.
Building objects: Passing arguments
Let’s imagine that we want to create a package called table that let the consumers build tables. The simplest way would be using the struct literal itself:
In most cases, we need to add some logic or property validations to build the object. For example, we can figure that the minimum legs of a table should be 3 to be stable. In these cases we need to use a constructor for the table:
Ok, simple, effective… but What happens if we need to add more properties to our type?
Every change of the constructor’s parameters will break compatibility and will force us to change every call and makes harder to understand and test your API. It also forces the consumer of the package to memorize the order of the parameters, which one are optional or mandatory and their default values.
A logical evolution of this design would be passing a configuration struct instead a list of arguments:
This approach give us the possibility to add more properties without breaking the API compatibility, however we still do not know which of these parameters are optional or mandatory, and which are their default values.
Building objects: Builder pattern
Another approach we can take is to implement the builder pattern. You can look for a full implementation here.
This is how our API would look:
With the builder pattern we get rid of the parameters order problem, the default
values problem(notice that int Legs()
method invocation we accept the 3 legs default value)
and makes the code easier to read and test.
In the other hand, we have to create a builder for every concrete type, and we don’t have the opportunity as a consumer to extend the behaviour of the constructor.
We can improve this design with Functional Options.
Building objects: Functional options pattern
In the post Self-referential functions and the design of options Rob Pike introduces the concept of the Functional Options and his motivation.
Functional Options is a functional pattern that lets us set the state of type value we want to create with a series of options.
An option is not more than a function that returns another function: a closure. This closure will be executed inside the constructor and set the state of the type value.
Formal definition
Now, let’s look to our previous table example using Functional Options.
Table example
Maybe it could seem a lot of code a first time, but the benefits justify this downside.
Benefits
- Makes code easier to read and test it.
- Makes more consistent the default values behaviour.
- Avoids breaking API breaks.
- Safe use of the API, avoids bad uses and values.
- Can be easily extended with our options implementation.
- Self documenting API.
- Highly configurable.
Practical use cases
The table example code is useful to understand the concept, but is more useful to see this pattern in real daily use cases.
Conclusions
Functional Options pattern lets us to define friendly APIs and make the architecture of our libraries more readable, testable, extendable and configurable.
For me, a good rule to follow while designing any private or public library/package exposing an API is “design your APIs like you were the consumer”.
However, remember that sometimes we don’t need this type of patterns, if a simple literal type value initialization fits your need, use it, don’t feel the need to create an unnecessary constructor or to use this Functional Options pattern.