Decorators in Go using embedded structs
The decorator pattern is a convenient way of extending functionality with minimal changes to existing code. The basic premise is to create a new type (the decorator) which wraps an existing one. Our decorator should implement the same interface functions as the wrapped type and forward function calls to the wrapped object with some extra functionality. Because the decorator implements the same interfaces as the type it wraps they are interchangeable.
In some languages creating a decorator type entails a bit of boilerplate. In Java for example one would have to declare an all new class with an instance variable for the wrapped object. Every interface function would then have to be manually overriden and forwarded to the decorated object, leaving room for human error. Go has a nice little feature which makes this process much less manual, embedded structs. Let’s try it out with a simple example.
Adding caching to an API client
Say we have a simple client for fetching and creating users with an external API. A basic implementation could look something like this:
1
2
3
4
5
6
7
8
9
10
11
package api
type HTTPClient struct {}
func (client HTTPClient) GetUsers() []string {
// Fetch all users from API and return their usernames.
}
func (client HTTPClient) CreateUser(username string) {
// Add a new user with the specified username.
}
Now say we want to make our client more efficient by caching the results of GetUsers
to lower the amount of API calls. One way to do this could be to add the caching to our HTTPClient
but that would make our type responsible for both API requests and caching, two pretty separate concerns. Another way would be to add caching wherever the HTTP client is used but that could possibly mean a lot of code changes in many different places.
A third is way is to use the decorator pattern and create a new type which decorates HTTPClient
. The new type would be interchangeable with the existing client which would minimize the need for changes to existing code. It would also only handle the actual caching, giving us a clear separation of concerns. For this we’ll need a Client
interface which can be used in place of HTTPClient
.
1
2
3
4
5
6
package api
type Client interface {
GetUsers() []string
CreateUser(string)
}
Next let’s create a new struct, CachedHTTPClient
, which embeds HTTPClient
.
1
2
3
4
5
package api
type CachedHTTPClient struct {
HTTPClient
}
That was easy enough. Does this really do anything? Yes, it does! This embedding will add a field to CachedHTTPClient
with an HTTPClient
object. Not only that it will also automatically get all the exported functions from HTTPClient
which will be forwarded to the wrapped object. Given this our new struct will also implement our Client
interface and CachedHTTPClient
is ready to be used, acting exactly as the object it wraps.
Our final step is to make GetUsers
use a cache. We can take control of GetUsers
by simply implementing it ourselves. In that case we need to manually invoke our wrapped objects function. We’ll use a hypothetical cache.Cache
object to handle our caching needs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package api
type CachedHTTPClient struct {
HTTPClient
usersCache cache.Cache
}
func (client CachedHTTPClient) GetUsers() []string {
// Check if there are any cached users.
cachedUsers, hasCache := client.usersCache.Get()
if hasCache {
return cachedUsers
}
// If there are no cached users we'll fetch them from the API and save to cache.
users := client.HTTPClient.GetUsers()
client.usersCache.Set(users)
return users
}
Notice that we never have to manually define CreateUser
if we don’t want to decorate it. This reduces the amount of boilerplate and makes our CachedHTTPClient
implementation more focused on its main task, caching. The effect becomes even greater with more exported functions on the decorated object.
It’s worth noting that there’s a caveat to using embedded structs. Embedding HTTPClient
will add a new exported field, making it possible to bypass the decorator and invoke the wrapped object directly using cachedClient.HTTPClient.GetUsers()
. Preferably the wrapped object should not be exported but to achieve that we would have to do without the embedding and do the manual function forwarding, hence there is definitely a trade-off to using this technique.