What even is “Dependency Injection”? (a practical example using Go)
I’m not satisfied with most explanations of dependency injection. I honestly don’t even use the term anymore but it’s so common online that I have to. I promise the concept is much simpler than you think it is. If you’ve struggled to understand it don’t worry, I had the same problem. This post will clear things up :)
Dependency injection is about being able to substitute one implementation of a thing for another implementation of a thing. To do this, you have to pass in the thing to something, instead of that something making the thing itself. Let’s work through an example
package main
type Entitlement struct {
AccountId string
CanTransfer bool
}
type EntitlementsClient interface {
GetEntitlement(accountId string) Entitlement
}
type Account struct {
UserId string
AccountId string
//...
}
type AccountsRepository interface {
GetByUserId(userId string) ([]Account, error)
}
Okay, so we have two interface
s here that both deal with accounts. One gets us accounts, given a userId
, and the other gets an entitlement ( which is just a thing that tells us what an account can do ). We’re not going to spend time looking at the implementations because they’re unimportant — I didn’t even implement them! But let’s see how dependency injection works..
type Service interface {
GetTransferAccounts(userId string) ([]Account, error)
}
type service struct {
accountsRepository AccountsRepository
aentitlementClient EntitlementsClient
}
// DEPENDENCY INJECTION
func NewService(acctsRepo AccountsRepository, entitlementsClient EntitlementsClient) service {
return service{
acctsRepo,
entitlementsClient,
}
}
func (s *service) GetTransferAccounts(userId string) ([]Account, error) {
accounts, err := s.accountRepository.GetByUserId(userId)
if err != nil {
return nil, err
}
output := []Account{}
for _, a := range accounts {
entitlement, err := s.accountEntitlementClient.GetEntitlement(a.AccountId)
if err != nil {
return nil, err
}
if entitlement.CanTransfer {
output = append(output, a)
}
}
return output, nil
}
And there it is!! That is literally dependency injection right there. The passing in of an interface
for our NewService
, the AccountsRepository
and EntitlementsClient
, is dependency injection — those are the dependencies we are injecting into the Service
.
Service
does not care how AccountsRepository
is implemented. It only cares that whatever is injected adheres to the AccountsRepository
interface
. The Service
is not creating dependencies itself, but is instead asking for them. And the benefit of this is we can provide any implementation of those interface
s to the Service
. We can provide a MockAccountsRepository
for testing — and this is the most likely thing you will do. You can provide a Postgres version, a Mysql version, a Redis version, a whatever version. No matter what you provide, the code for the Service
does not need to change, because it doesn’t know what implementation you’re providing anyway.
Here’s what the use of our “dependency injection” allows us to do in a test
type mockAccountRepository struct {}
func (r *mockAccountRepository) GetByUserId(userId string) ([]Account, error) {
return []Account{{"1", "2"}}, nil
}
type mockEntitlementsClient struct {}
func (r *mockAccountRepository) GetEntitlement(accountId string) (Entitlement, error) {
return Entitlement{"2", true}, nil
}
//Okay we have a mocked version of each 'dependency'
accountRepository := mockAccountRepository{}
entitlementsClient := mockEntitlementsClient{}
service := NewService(&accountRepository, &entitlementsClient)
x, _:= service.GetTransferAccounts("1234")
We create two versions of the dependencies and we can pass them to our Service
. Again, it does not matter how the dependency is implemented. The Service
is decoupled (only used through an interface
instead of directly) from the dependencies, because it’s only using the dependencies through an interface
. The use of an interface
is the decoupling — you’re decoupled from a specific implementation. The dependencies can be tested independently of the Service
and the Service
can be tested independently of the dependencies. And realize that this test code is basically the code you’d use for different implementations. You’d have a Postgres one or a Redis one. It’s all the same thing.
The hardest thing about “dependency injection” is the words.
