Suppose we want to write some Go to query a database. We could begin with a simple struct to hold our implementation.
type AccountsDB struct {
db *sql.DB
}
And attach some methods to it.
func (d *AccountsDB) GetAccount(ctx context.Context, id string) (*Account, error) {
// ...
}
// ...
Then, write a function to create it.
func NewAccountsDB(config *Config) *AccountsDB {
// ...
}
Now, we’d have a way to create an object with a single responsibility: accounts database queries. With this separation of concern, another type of object, let’s say the AccountsService, would have to consume it.
type AccountsService struct {
db *AccountsDB
}
AccountsService would call methods from AccountsDB and do some stuff around them.
func (s *AccountsService) GetAccount(ctx context.Context, id string) (*Account, error) {
// ... - different types of checks
account, err := s.db.GetAccount(ctx, id)
// ... - handle errors
return account, nil
}
Problem
Until now, we’ve kept the level of abstraction very low. We simply created 2 struct types, and one calls the other. If all of a sudden, we completely stopped to think, one could argue that the job is done. But we don’t need to go far to discover a big limitation in our code.
Suppose we want to write some unit tests for AccountsService. When it calls AccountsDB, we only want to acknowledge that it’s been called correctly. We don’t want these tests to be impacted by whatever happens inside AccountsDB, including calls to a real database that would need to be up and running.
We want the subject of our unit tests to only be the code inside their unit. So in fact, we would need to pass a different implementation of AccountsDB to AccountsService. A mocked implementation, only for our tests.
What we’ve just discovered here is a need for an interface.
Solution
Don’t design with interfaces, discover them.
This quote from Rob Pike, one of the creators of Go, reminds us that Go’s design is oriented towards minimalism and simplicity. So if we want to write idiomatic code, we must stay away from unnecessary complexity.
As we just did, be pragmatic and start from the concrete needs of your project. Increase the level of abstraction if, and only if, it’s a true necessity.
In our case, we now want to do just that. We want to create an interface to represent a repository of accounts that will contain the signatures of the methods we had in AccountsDB.
type AccountsRepository interface {
GetAccount(ctx context.Context, id string) (*Account, error)
// ...
}
Then, we can redefine our AccountsService to receive our new interface instead of the concrete type.
type AccountsService struct {
repo *AccountsRepository
}
Next, for our unit tests, all we need to do is pass a mocked version of AccountsRepository to AccountsService.
mockedRepo := newMockedAccountsRepo()
service := api.NewAccountsService(mockedRepo)
Finally, we can use the mocked repository inside our unit tests. We can make sure that the expected calls happen, and return fake values if they happen.
mockedRepo.EXPECT().GetAccount(ctx, accountID).Return(testAccount, nil)
account, err := service.GetAccount(ctx, accountID)
Note: mocks can easily be generated from an interface using a tool like Mockery (github).
Conclusion
Only when we discover the necessity for an interface, do we decide to create one. Not for a hypothetical need in an unforeseen future, nor the beauty of the “ultimate” abstraction. And we define it from the point of view of the code that will consume it, it’s its requirement after all. While the code that produces concrete implementations just has to satisfy the interface’s “contract”.
In Go, interfaces are implicitly satisfied, meaning that a struct only needs to implement the methods defined in an interface to be considered a valid implementation of that interface. We don’t need to write “X implements Y” explicitly, and X can implement many different interfaces.
This feature of the language goes hand-in-hand with the consumer/producer pattern that we’ve just described. They can help us strike the right balance of abstraction, creating the right interfaces in the right spots.