Mortar

Mortar DesignBuilder Pattern

You’re probably used to functional options pattern. However, we found Builder pattern to be very useful, here we will explain why.

Motivation

  • “See” all the options without searching for them.
  • Partial Builders:
    • Override previously set values

Library usage within the organization

+---------------------+            +-------------------------------+           +-------------------------+
|  Library Developer  | +--------> | Platform/Infra/Ops Developer  | +-------> |  Integration Developer  |
+---------------------+            +-------------------------------+           +-------------------------+

Develops the library to be used    Pre-configure the library specifically      Set final values
in different scenarios, expose     to their organization.                      according to specific use case.
different options.

     Library defaults                  Predefined defaults                       Override defaults

Our Library

Let’s pretend we are building a library. Our library will have a constructor:

func NewLib(options ...Option) Library {}

type Option func(*libConf)

Configuration will be stored in libConf:

type libConf struct {
    address string
    // other options omitted for clarity...
}

func Address(addr string) Option {
    return func(cfg *libConf) {
        cfg.address = addr
    }
}

What is it about then?

The purpose of this pattern is to “introduce” or “unite” between a library developer and the platform/infra developer as shown in the above chart. We want to introduce predefined defaults that are specific to the organization/use case and are not just library specific. Let’s examine our Library again, we have to set an address since our library needs to connect to some server. We can look at this problem from difference perspectives:

Library Developer

The library developer has no idea about your IP address, right? Hence the exposed Option to set the address. However, to find that option or any others you will need to look at the source code, right ?

Infrastructure/Platform developer within your organization

An Infrastructure/Platform developer has already setup this server and now needs to tell every “Integration developer” what that address is.

Integration Developer

Either knows or not about this server IP that was introduced by the Platform/Infrastructure team but still needs to have a way to override it, because this IP can change with time or there is a local server that is used during tests.

Builder

type LibBuilder interface {
    Address(addr string) LibBuilder
    // ... additional options omitted for clarity
    Build() Library
}

So far nothing new, you can even look at it as a set of options without the ability to extend and not break the API. And you’re right, however it’s not intended as a drop-in replacement for functional options.

Linked list

The Builder implementation is based on a linked list:

                                               +-------------------------------+
                                               |                               |
                 +---------------------------->+         Configuration         |
                 |                             |                               |
                 |                             +-------+------------------+----+
                 |                                     ^                  ^
                 |                                     |                  |
       +---------+-----------+         +---------------+-----+         +--+------------------+
       |                     |         |                     |         |                     |
+----->+  Address Option     +-------->+  Max     Option     +-------->+  Min     Option     |
       |                     |         |                     |         |                     |
       +---------------------+         +---------------------+         +---------------------+

Each Option is presented as a function on the builder interface and added a to a list of previous options.

Each Builder function is an alias to a functional option

Overriding predefined defaults

Since it’s a list of options, we can add a new option to the end of a list that will actually override a previous one:

                                               +-------------------------------+
                                               |                               |
                 +---------------------------->+         Configuration         |
                 |                             |                               |
                 |                             +-------+------------------+----+
                 |                                     ^                  ^
                 |                                     |                  |
       +---------+-----------+         +---------------+-----+         +--+------------------+         +---------------------+
       |                     |         |                     |         |                     |         |                     |
+----->+  Address Option     +-------->+  Max     Option     +-------->+  Min     Option     +-------->+  Address Option     |
       |                     |         |                     |         |                     |         |                     |
       +---------+-----------+         +---------------------+         +---------------------+         +-----------+---------+
                 ^                                                                                                 |
                 |                                                                                                 |
                 +-----------------------------------------OVERRIDES-----------------------------------------------+
     +                                                                                           +
     |                                                                                           |
     +---------------------------------------Predefined defaults---------------------------------+

Implementation

Finally, let’s just build it:

import (
	"fmt"
	"container/list"
)

type libConf struct {
	address string
}

type Library string
type LibBuilder interface {
	Address(addr string) LibBuilder
	// ... additional options omitted for clarity
	Build() Library
}
type libBuilder struct {
	ll *list.List
}

func Builder() LibBuilder {
	return &libBuilder{
		ll: list.New(),
	}
}
func (b *libBuilder) Address(addr string) LibBuilder {
	b.ll.PushBack(func(cfg *libConf) {
		fmt.Printf("using %s as an address\n", addr) // for debug
		cfg.address = addr
	})
	return b
}
func (b *libBuilder) Build() Library {
	var cfg = new(libConf) // empty conf
	// Iterate on the linked list
	for e := b.ll.Front(); e != nil; e = e.Next() {
		f := e.Value.(func(*libConf)) // extract and cast
		f(cfg)
	}
	// at this point cfg will be populated
	return Library(fmt.Sprintf("We are going to call [%s] address", cfg.address)) // or something similar
}

Once we have our builder, we will use it as follows:

func makeLibrary(partialBuilder LibBuilder) Library {
    // Here we have passed previously defined builder, one that has some defaults already in it.
    // Now we can either use it as is or override it.
    builder := partialBuilder.Address("5678")
    return builder.Build()
}

func main() {
	var builder = Builder().Address("1234") // predefined builder
	lib:=makeLibrary(builder)
	fmt.Println(lib)
}

Now, if you run it the output will be:

using 1234 as an address
using 5678 as an address
We are going to call [5678] address