Fabian Lindfors

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.