Like all core Laravel features, sending mail is made easy and convenient by the clean and expressive API that it…
In this blogpost I’m going to explain how to keep your code clean using decorators. By applying decorators the open close principle is maintained, code becomes easier to extend and easier to adjust.
Background
According to wikipedia the decorator pattern is a design pattern that allows behaviour to be added to an individual object, without affecting the behavior of other objects from the same class. In go this means that we extend the functionality of a function or method(s) of a struct without changing the original functions or method(s).
Consider the following interface:
type Adder interface {
Add(x, y int) int
}
We can implement this interface and use it somewhere in our code
package main
type adderImpl struct {}
func (adderImpl) Add(x, y int) int {
return x + y
}
func main() {
var a Adder = adderImpl{}
fmt.Println(a.Add(1,2))
// prints => 3
}
This is idiomatic go code and works fine for adding numbers. Because we have an interface with 1 function we can also create a function signature for it, which implements the Adder
interface. This way we can use it even more easily, it’s the same principle as the http.HandlerFunc
from the standard library.
type AdderFunc func(x, y int) int
func (a AdderFunc) Add(x, y int) int {
return a(x, y)
}
What happens in the code above is that we define a type which is a function. On this function type we implement the original Adder
interface and propagate the Add
call to the AdderFunc
. This way we won’t need to implement the interface everytime on a struct and can just use a function to implement the behaviour.
package main
func main() {
a := AdderFunc(
func(x, y int) int {
return x + y
},
)
fmt.Println(Do(a))
// prints => 3
}
func Do(adder Adder) int {
return adder.Add(1, 2)
}
Note the type conversion for the anonymous function to the AdderFunc
is required because it’s not able to implicitly convert an anonymous function to the Adder
interface directly. If the Do
function would accept an AdderFunc
instead of an Adder
it is possible to pass in the anonymous function directly and the compiler knows how to convert the function to the AdderFunc
type implicitly.
Middleware
Let’s say we wan’t to add logging capabilities to the adder function. We can do a naive way and add logging to the original function but this will introduce a dependency on the log package in our business domain logic. It also violates the single responsiblity and open close principle because the code is not only adding numbers anymore but also logging. Below I illustrate an example of the wrong
way.
Wrong way
package main
func main() {
a := AdderFunc(
func(x, y int) (result int) {
defer func(t time.Time) {
log.Printf("took=%v, x=%v, y=%v, result=%v", time.Since(t), x, y, result)
}(time.Now())
return x + y
},
)
fmt.Println(Do(a))
// prints => 2009/11/10 23:00:00 took=0s, x=1, y=2, result=3
// prints => 3
}
func Do(adder Adder) int {
return adder.Add(1, 2)
}
There is a lot happening in the code above, after the x + y
is added there runs another function using defer
which logs the input and output values, aswell as the time it took to calculate x + y
. Currently our code is all in main
, this is not really a violation but when we start seperating the code in packages the logging code starts to occur everywhere.
A better approach would be to apply the Decorator
pattern often called Middleware
in Go. This pattern at its simplest is running code before and/or after our original code. The Middleware
pattern is the decorator pattern applied in Go
. When using custom Middleware
functions wrapping our interface
we can add behaviour on top of the original behaviour. Our Middleware
function takes in an Adder
and returns a new decorated Adder
.
// Middleware function, this function takes in a `Adder` and returns a new `Adder`.
type AdderMiddleware func(Adder) Adder
func Wraplogger(logger *log.Logger) AdderMiddleware {
return func(a Adder) Adder {
// Using `AdderFunc` to implement the `Adder` interface.
fn := func(x, y int) (result int) {
defer func(t time.Time) {
logger.Printf("took=%v, x=%v, y=%v, result=%v", time.Since(t), x, y, result)
}(time.Now())
// Propogate call to original adder
return a.Add(x, y)
}
// Return a new `Adder` wrapped with the loggin functionality
return AdderFunc(fn)
}
}
As you can see the AdderFunc
type is used, this type makes sure that the fn
function is converted to implement the Adder
interface. The fn
closure calls the logger after the original Adder
is called. The beauty of this is that the WrapLogger
function doesn’t know any of the internals of the original Adder
it only extends the behaviour of the Adder
by loggin the input and output. This way we adhere to the open close principle. By combining the power of first class functions and the middleware pattern we are able to add additional behaviour on top of the original behaviour.
Middleware
can be written for any type of functionality you want to add to an Adder
. Consider the case that we also need to cache the results, because adding our numbers takes to much compute time, we can easily add a caching layer on top of our Adder
by writing a Middleware
function. I’ll be using a sync.Map
from the std library here to implement the caching.
NOTE that this is pure for illustration purposes and caching the results will likely be a performance hit.
func WrapCache(cache *sync.Map) AdderMiddleware {
return func(a Adder) Adder {
fn := func(x, y int) int {
key := fmt.Sprintf("x=%dy=%d", x, y)
val, ok := cache.Load(key)
if ok {
return val.(int)
}
result := a.Add(x, y)
cache.Store(key, result)
return result
}
return AdderFunc(fn)
}
}
The sole purpose of the WrapCache
function is to cache the response of the Adder
it’s wrapping. The original Adder
has no clue about this caching functionality. If we get a new requirement to only cache even number we only have to change the WrapCache
function to cache only on even numbers. Also if we need to use an external cache like redis or memcache we can write a new caching middleware function for this type of cache and use that to wrap the original adder instead of using the WrapCache
method. This allows us to easily switch between implementations (also interfaces
can be used to depend on only abstractions in the WrapCache
method)
Testing
Testing using this pattern becomes easier aswell, we can test the business logic seperatly from the caching logic and logging logic. In our business logic we only have to test the original Adder
functionality, in for example the WrapCache
function we only have to test if the result is cached, we don’t care about the result itselfs only if it’s correctly cached or not.
Chaining
When you have many functions decorating the original behaviour combining them can become messy.
func main() {
a = WrapCache(&sync.Map{})(WrapLogger(log.New(os.Stdout, "test", 1))(a))
a.Add(10, 20)
}
For this we can have an elegant solution called chaining, this solution makes it more human readable to see how the wrapping works.
func Chain(outer AdderMiddleware, middleware ...AdderMiddleware) AdderMiddleware {
return func(a Adder) Adder {
topIndex := len(middleware) - 1
for i := range middleware {
a = middleware[topIndex-i](a)
}
return outer(a)
}
}
The above code loops through the given middleware functions in reverse order and inserts the Adder
in it. With every iteration the Adder
is overwritten to the wrapped adder which is used as input for the next middleware
. The reason why the loop is in reverse order is because this way the wrapped function is called in the order the middlewares are declared. That means that the first middleware function is the outermost middleware wrapper. Chain is also a middleware function, but instead of having to do all this wrapping manually, the Chain
function can be used like this, making the main function way cleaner:
func main() {
var a Adder = AdderFunc(
func(x, y int) int {
return x + y
},
)
a = Chain(
WrapLogger(log.New(os.Stdout, "test", 1)),
WrapCache(&sync.Map{}),
)(a)
a.Add(10, 20)
}
When reading this codes it more obvious what is happening, adding new middleware functions is also a bit easier and doesn’t make the code messy.
Further thoughts
This pattern can be really usefull to seperate the business logic from things which don’t belong in the business logic, like caching, logging and metrics. It also allows you to think in terms of small composable interfaces and how to cleanly add additional features on top of them without changing the original code and behaviour. In the end your main function starts to look very clean and clear for a future developer. My main
functions started to look like this:
gc := geocoder.Chain(
geocoder.WrapLogger(svcLogger.WithField("geocoder", "google-maps")),
geocoder.WrapMetrics(mockMetrics),
geocoder.WrapLRUCache(mockMetrics, 8192),
)(geocoder.Googlemaps(gm, "NL"))
svc := geoparser.Chain(
geoparser.WrapLogger(svcLogger.WithField("geoparser", "parser")),
geoparser.WrapMetrics(mockMetrics),
)(geoparser.WrapService(mockMetrics, p, gc))
My functions became easier to test and were also easier to adjust, this pattern allowed for better maintainability overall. The pattern can also be applied to interfaces with multiple functions, but this is left as an exercise for the reader.