In software engineering, over the years folks have developed many patterns, techniques and refactoring maneuvers. Some have been long forgotten, while others have stood the test of times.
Such a long-standing technique is dependency injection. It is a concept and a programming technique where a construct is passed (injected) to another construct that depends on it.
In Golang, like in other languages, we use it to simplify our code and make it more testable. But, how much more? Let’s code through an example and see how dependency injection can help us out.
Imagine we are building an e-commerce website where people buy goods and we ship them the goods. When their order ships, we want to inform them through a notification.
Let’s look at a very simple simulation of sending an SMS notification over an API:
For the sake of the example, we’ll simulate a network call in the
with arbitrary sleep time, so the function takes a few seconds to execute:
Let us see a simple test for the
InformOrderShipped function and what kind of
challenges it brings:
While these tests do test the functionality of
InformOrderShipped, it already
has two issues:
- It lasts three seconds because of invoking
sms.Sendsleeps for three seconds.
- There’s no way to easily test what would happen if
sms.Sendactually returns an error.
Issue number one is something we can ignore for now, as it’s only a side-effect
of the implementation of
sms.Send. Issue number two, the lack of an easy way
sms.Send, is larger so we will address it first.
Injecting the ability to send SMSes
The simplest maneuver we can do in
InformOrderShipped, instead of depending
sms package, is to pass in the
Send function as a function closure
Because Go allows us to provide the function as an argument, all we need to do is to specify its arguments and return types and invoke it in the main function. Just by having this in place, we achieve:
- Injecting the dependency on the
sms.Sendfunction, using a type rather then importing the actual function.
- We remove the explicit dependency on the
- We make our code easy to mock and test.
To prove these points, we will modify the test to include the two scenarios, when the SMS is successfully sent and when it returns an error:
In the test we use the table-driven approach, where we define two different test cases: when the SMS sending fails and when it does not.
InformOrderShipped function expects a function as a third
argument, in our test we create a function mock that has no functionality but
returning an error value.
This allows us to pass in the mocked function, easily control its return value and avoid waiting for three seconds for the test to finish. We decouple our code, we make it more testable and, on top of that, it executes instantly.
But, can we take this further?
Notifications are more than SMS
SMSes are a bit outdated. Nowadays we very often rely on our cellular internet connections, native apps and push notifications to get real-time information about our orders. Also, as much as we dislike it, email is still the online communication king.
But you know what’s the best of the three? All of them. People want to be kept in the know, especially when the items that they spend their hard-earned monies on have been dispatched.
So, how can we make our code even more flexible, while keeping it easy to test and maintain?
Interfaces to the rescue!
Golang interfaces are a powerful construct. They are simple, named collections
of method signatures. Interfaces are implicitly implemented, meaning a type
implements an interface by implementing the functions specified in the
interface. No need for explicit
implements statements. (Yes, like yours
Let’s introduce an interface called
Sender, which defines a
Those of you that are very attentive will notice that the
Send function in
Sender has the same signature as the
sms.Send function from the earlier
example. That’s intentional - we want any implementor of the
to be able to send a notification to a
Let’s make a
sms.Dispatcher type which will implement the
and use it in our code:
Now, we can pass an instance of the
Dispatcher type in the
InformOrderShipped function, which will invoke its
Send function. Given that
we already have the
Sender interface defined, we can use it as an argument:
Dispatcher already implements the
Sender function, which means
that we can pass an instance and it will work. Here’s an example:
How does such a change influence our tests? Interestingly, it doesn’t. While we
lose the ability to pass in a function closure as an argument, we can still
create a mock type that implements the
Sender interface and use it as
argument to the
Just a simple
mockSender type does the trick. Because it implements the
Send function and due to Go’s implicitly implementing interfaces
is also a
Sender. That means we can use it as an argument to the
The flexibility that the
Sender interface provides us cannot be overstated.
Although the example is small and trivial, introducing another type of notification
InformOrderShipped is simple:
This would result in no changes in the
InformOrderShipped function, while the
Send method can be used as the one of the
With dependency injection in action, we were able not only to decouple our
InformOrderShipped function from the
sms package, but also by using an
interface for dependency injection we got to use polymorphism. Simply put,
polymorphism through interfaces allowed us to send SMSes and push notifications
in the same