What is dependency injection?
Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID\’s dependency inversion and single responsibility principles, in order to write a good programme.
Let’s refresh our memory with the SOLID principles:
- Single Responsibility Principle: By creating small interfaces, we define obvious responsibilities for implementing classes. It makes it easier to follow the SRP, especially when we make our classes implement only a handful or even a single interface.
- Open/Closed Principle: With loose coupling and hidden implementations, following OCP is also more straightforward. Since the client code doesn’t rely on the implementation, we can introduce additional subclasses as needed.
- Liskov Substitution Principle: LSP is not directly connected to this technique. However, we must take care when we’re designing our inheritance hierarchy to follow this principle, too.
- Interface Segregation Principle: ISP isn’t a result but a good practice to follow when we’re programming interfaces. Note that we already talked about the importance of defining small, well-defined responsibilities. Those notes were hidden hints to follow ISP.
- Dependency Inversion Principle: By relying on abstractions, we already did the majority of the work to follow DIP. The last thing to do is to expect dependencies from an external party instead of instantiating them internally.
source: Baeldung
Before we discuss in more detail Dependency Injection and how to implement SOLID best practices into our Golang application, let’s have a look at what happens when we do not follow these principles and we couple the dependencies.
For example, we need a Service that needs to store in the database and send an email notification after storing.
You can just go ahead and write all of that on a single function call.
type Service struct {
}
func (s *Service) doTheThing() error {
//initialize the db connection
//store on the db
//initialize the email service
//send email
}
This works, but you are coupling the dependencies and here is where the problems start. As you can see from the example, before we are able to store in the DB we will need to initialize the connection, and the same thing will need to happen with the email service provider.
That’s our first problem. We are building the dependencies inside the function, which is an anti-pattern. The moment someone calls this function it’ll cause your application to crash, and your users will start getting error messages.
Another issue with coupling the dependencies, is that each function call will be creating a new connection, which might create problems if the disconnections are not properly handled.
Also, think about what will happen if someone else in our organisation follows the same approach in another function. We will have not one, but two places with the same code, creating database connections that can cause errors, the code starts to get messy, and it will be difficult to maintain.
This is one of the many reasons why we usually build external dependencies on application initialization, and provide them to the different services using dependency injection. By decoupling the dependencies, we are able to detect problems earlier, we avoid duplication of functionalities, and we prevent the creation of a new connection every time we call the function.
Now another question. If we want to test this functionality using a mock database or a mock email service, how would we inject the mock? The answer is that we can’t, since every time we call this function, the database and the email service will be initialized within the function, which is definitely not the right way.
Then how to do it the right way?
We know our service needs to store in the database and send an email, and we can’t build the dependencies inside the function, because it will create a lot of problems. Therefore, we will start by decoupling them:
Continuing with our example, we will have the 3 different components:
- The one that handles the DB logic
- The one that handles the Email notifications logic
- Our service that executes the business logic by calling the above components
type Db struct {
}
func (db *Db) connect() error {
//open the db connection
}
func (db *Db) store() error {
//store on the db
}
type EmailSender struct {
}
func (eS *EmailSender) connect() error {
//connects with the email service
}
func (eS *EmailSender) sendEmail() error {
//send email
}
type Service struct {
//Need a DB
//Need an EmailSender
}
func (s *Service) doTheThing() error {
//store on the db
//send email
}
As you can see, we are no longer calling the DB and the email service initialization inside our function. Instead, we are just calling the functionality we need, assuming the connection will be handled when these dependencies are created at the activation of the application.
But we have introduced 2 dependencies into our service that we need to provide, otherwise our function won’t work.
How do we provide them?
One thing we can do is compose our Service object by adding the dependencies and then call them on our function:
type Service struct {
Db *Db
EmailSender *EmailSender
}
func (s *Service) doTheThing() error {
//store on the db
s.Db
//send email
s.EmailSender
}
//you might want have a constructor to inject the dependencies
func New(db *Db, emailSender *EmailSender) *Service {
return &Service{
Db: db,
EmailSender:emailSender,
}
}
This is kind of similar to what you will do in Spring (Java) where you will autowire a component dependency to some other, except we don’t have the @Autowired, nor all the Spring magic! This is pure dependency injection, based on a constructor method, where you initialize your components and provide them when constructing a new function.
Now let’s have a look at what we are doing here. We are passing the complete DB and EmailSender functionalities to our new Service instance, and this should work. But what happens if we just want to use only one functionality – if we just need to send an email for example? There might be a lot of other functionalities that the EmailSender has and which we don’t really need on this service.
So here we have a new problem. And lets not forget that we might have fixed the decoupling and the dependencies injection, but we still haven’t solved how to inject the mocks.
So we still have 2 problems to solve…
Let’s take a quick look at the constructor we created:
func New(db *Db, emailSender *EmailSender) *Service {
return &Service{
Db: db,
EmailSender:emailSender,
}
}
This constructor only enables you to inject a DB and an EmailSender type, which means it will be impossible to pass a DbMock or an EmailSenderMock. The compiler will reject saying you are passing the wrong type. So how do you solve this?
And what happens if you want to provide a different emailSender? Would you have another object for that? Of course not!
This is why we will introduce interfaces -because you should always program to an interface, not to an implementation.
This is effectively the Dependency Inversion Principle. Doing so allows you to replace, intercept or decorate dependencies without the need to change consumers of such dependency. In other words, by programming to interfaces you should be able to provide different implementations with the same code (hey this solves our mock problems!).
Introducing interfaces
An interface can be thought of as a contract between different parts of a system.
In software, this means that if you know that contract, you can build your business logic around it and your business logic won\’t change. The only parts that will change will be the ones implementing that contract.
Therefore if we have an interface for our EmailSender with a method send email, we can build our business logic without thinking if that EmailSender is Gmail, Outlook, or even a custom one. We are able to abstract our business logic from the real provider.
The part that implements the interface needs to provide functionality for all the methods declared in the interface contract..
Golang (Go) interfaces are different from other languages
In Go, the interface is a custom type that is used to specify a set of one or more method signatures and the interface is abstract, so you are not allowed to create an instance of the interface.
But you are allowed to create a variable of an interface type, and this variable can be assigned with a concrete type value that has the methods the interface requires.
In other words, the interface is a collection of methods as well as a custom type.
type MyInterface interface {
DoSomething()error
}
In this example we are declaring an interface “MyInterface” that has a function DoSomething and throws an error.
How to implement interfaces into our Service?
In JAVA or other languages, you will do something like:
public class MyInterfaceImplementation implements MyInterface {
//implement DoSomething
}
And you will need to implement the DoSomething function.
In Go it is a little bit different:
Let’s go back to our example:
type Service struct {
Db *Db
EmailSender *EmailSender
}
How can we convert Db and EmailSender to interfaces so we can pass different implementations?
Since the interfaces are types, we can do something like:
type Service struct {
Db interface {
Store (s something) error
}
EmailSender interface {
Send (email Email) error
}
}
By doing this, we can inject whatever object instance that implements this interface, so for example if we have the following:
type EmailSender struct {
}
func (eS *EmailSender) Send(email Email) error {
//logic for sending the email
}
..and we inject this into our service on initialization, it will work because it is implementing the interface.
But wait, where is the implement keyword?
This is why we say in Golang the interfaces are implicit, if the functions your struct matches what the interface needs, you are good to go.
So our example will look something like this:
type (
Db interface {
Store (s something) error
}
Service struct {
EmailSender interface {
Send (email Email) error
}
}
//New – initializes our service object with the dependencies
func New(db *Db, emailSender *EmailSender) *Service {
return &Service{
Db: db,
EmailSender:emailSender,
}
}
func (s *Service) doTheThing() error {
//store on the db
s.Db
//send email
s.EmailSender
}
What’s different to what we had initially? Well now EmailSender and the Db are type interfaces, so we can inject whatever we need that matches the interface and this helps us achieve our next goal which is Unit Testing!
Unit Testing
Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinized for proper operation.
Why is Unit Testing and coverage important?
Even though having more than 80% coverage on your software doesn’t guarantee everything will work, it helps you prevent or detect bugs in early stages, and prevents any software change to break the existing functionality, so it’s really important!
Ok but how to apply Unit Testing in Golang with the things we just saw?
Now that you know a little more about interfaces we can use them to create mocks.
By just applying the same principle, we can just go ahead and create a mockStruct that has the necessary functionality, and inject that into our service like the following example:
type EmailSenderMock struct {
}
func (eS *EmailSenderMock) Send(email Email) error {
//logic for simulating we are sending the email
}
type DBMock struct {
}
func (db *DBMock) Store(s something) error {
//logic for simulating we are storing on the db
}
With this simple approach we are able to mock whatever we need, without the need of using any library or framework like we would do in Java (using mockito for example). And when we are writing our tests, we will just create a new instance of your service using these mocks.
func TestService_doTheThing(t *testing.T) {
dbMock := DBMock{}
emailSenderMock := EmailSenderMock{}
service := New(dbMock, emailSenderMock)
err := service.doTheThing()
if err != nil {
t.Fail()
}
}
Final thoughts
Now you should have a better understanding of how to easily implement Dependency Injection and Unit Testing in Golang.
Following the SOLID principles, implementing Dependency Injection and Unit Testing are really important to help you decouple, test and have a cleaner, more maintainable code, preventing things from breaking and help anticipate undesired errors.
Next time you build a functionality check that you are doing it the right way and you are not coupling dependencies!