Composing Interfaces in Go
Composing interfaces in Go is one of the features I like the most in the language. In this article we are going to write a real use case composing small interfaces.
To demonstrate this concept, let’s imagine a hypothetical scenario where we have two structs which manage users and execute http calls:
Sync
and Store
are structs responsible for managing users in our system, given both executes http requests they expect to receive as a dependency a structure which satisfies the HTTPClient
interface.
Let’s see the HTTPClient
definition:
Nice, we have one struct for managing users sync, and other for managing store logic. It sounds to be very easy to test, given they do one thing and do it well, also both accepts an interface as argument. The way we designed our code, all we need to do is to create a HTTPClientMock
and that’s it.
Let’s see how the implementation of Sync
unit test would look like:
Our test works pretty well, but note that Sync
doesn’t use the Get
method from HTTPClient
Clients should not be forced to depend on methods they do not use. –Robert C. Martin
Another downside is we might have to implement every new method, which might be added in the HTTPClient
e.g Patch, Delete, Put
to our mock struct, making much harder to test and increasing the complexity while reading the code. If you change the Get
method signature will affect Sync
tests which doesn’t use this method. It’s a unnecessary coupling and we should get rid of.
In this hypothetical example we are mocking theHTTPClient
with two methods only, but imagine a scenario where you need to expose a new handler for listening to the messaging system, and this handler responsibility is to receive a payload and store it on the database:
The AMQPHandler
only cares about the Add
method which manages atomic operation for storing the user, and as you might have guessed, the Repository
mock would have the following anomaly:
Due to our poor design we have no option, every client of Repository
must implement the methods defined by the contract.
This is the opposite of what interfaces in Go tend to be, in Go interfaces should be small, limited to one or two methods, and this case Repository
absolutely exceeds it, making extremely hard to re-use due it’s huge contract.
Composing
The bigger the interface, the weaker the abstraction, Rob Pike
Back to our program for managing users, the Sync
struct should be expecting to receive an interface which satisfies the Post
method. The one that needs both Post
and Get
is Store
So let’s re-design Sync
a little bit:
The code speaks by itself, it’s much clear what is being passed to NewSync
constructor, much easier to test and we don’t need to manage the whole contract of HTTPClient
interface.
Let’s see how Store
is managing to receive HTTPClient
as a dependency:
To be honest there are no news so far, the way HTTPClient
was designed is pretty much the same as ReadWriter:
type ReadWriter interface {
Reader
Writer
}
Leveraging interface composition allowed us to make the dependencies clear.
The astute readers might have noticed some TDD in action here, without unit tests I would say one is unable to achieve such design in the first attempt. Also note no external libraries were required for asserting, mock and so on — all credits to
You might be wondering how would be the implementation of HTTPClient
?
As simple as that, HTTPClient
just need to implement Post
and Get
. Note we are not returning any interface like:
type HTTPClient interface {
Post(...)
Get(...) }func New(...) HTTPClient {
// returns a public/private struct
}
The implementing package should return concrete (usually pointer or struct) types, see CodeReview in the Golang repository.
Let the consumer package declare it, in this case user
pkg:
Finally we tie all dependencies in our main.go
pkg creating and passing them downstream to Sync, Store
and so on:
Conclusion
From now on with this real world example, I hope you start using Interface Segregation Principle in order to write more idiomatic Go code with clear dependencies, easier to test and to reason about.
In the next post, we will make HTTPClient
resilient to failures with circuit breakers and retry logic, stay tuned.
Show me the code
You can check the source code containing the full implementation
Acknowledgements
Thanks to my friends Bastian and Felipe for reviewing this article.