When I was reading my daily Medium stories, I saw a good one that lead me to shift the way on which I’ve been using the interfaces in Golang. Those examples not even were necessary to be extracted from any high-rated Golang book, even those examples have been always present in “The Go Programming Language” a.k.a “Blue Book” written by Alan Donovan and Brian Kernighan.
I’ve always written interfaces in Golang, specifically when I’ve been working with repositories in a coarse way like the example following below:
// I always thing in this kind of repository interface:
type Repository interface {
Create(ctx context.Context, data Data)
Read(ctx context.Context, pk string)
Update(ctx context.Context, pk string, newData Data)
Delete(ctx context.Context, pk string)
}
I realized this approach violates the “I” SOLID principle (“I” stands for “Interface Segregation” principle) which the definition is the following below:
“Clients shouldn’t be forced to depend on methods they do not use”
If I segregate all those methods above into “ers” interfaces, I will get the following code:
type Creater interface {
Create(ctx context.Context, data Data)
}
type Reader interface {
Read(ctx context.Context, pk string)
}
type Updater interface {
Update(ctx context.Context, pk string, newData Data)
}
type Deleter interface {
Delete(ctx context.Context, pk string)
}
Thinking specifically on this approach, I could combine different interfaces which automatically allow me to segregate all those I don’t need to use as is shown below:
type CreateReader interface {
Create(ctx context.Context, data Data)
Read(ctx context.Context, pk string)
}
The example shown in “The Go Programming Language” (ch. 7, page 174) refers to the same approach with the interfaces Writer and Reader both found in the package io:
package io
type Reader interface {
Read(p []byte)(n int, err error)
}
type Writer interface {
Write(p []byte)(n int, err error)
}
type Closer interface {
Close() error
}
It makes easier the life of every Golang developer when they want to get deeper into language intricacies and the art of writting programs. This approach resembles a little about the structs embedding and it works in the same way.
Until now, I’ve been talking about the “I” SOLID principle, but there’s a curious “flaw” that I’m realizing when I write code in Golang: returning of interfaces as types when I create a “constructor” function. I’m going show you, dear reader a simple example of my “flaw”:
// ./solid/domain/entities/data.go
package domain
import "time"
type Data struct {
Id int
Name string
Description string
CreatedAt time.Time
UpdatedAt time.Time
}
// ./solid/domain/repositories/repository.go
package repository
import (
"context"
"github.com/yescorihuela/solid/domain"
)
type Repository interface {
Create(ctx context.Context, data entities.Data) error
Read(ctx context.Context, pk string) (*entities.Data, error)
Update(ctx context.Context, pk string, newData entities.Data) error
Delete(ctx context.Context, pk string) error
}
// ./solid/infrastructure/postgresql.go
package infrastructure
import (
"context"
"database/sql"
"github.com/yescorihuela/solid/domain"
"github.com/yescorihuela/solid/repository"
)
type postgresRepository struct {
db *sql.DB
}
func NewPostgresRepository(db *sql.DB) repositories.PostgresRepository {
return &postgresRepository{
db: db,
}
}
func (pr *postgresRepository) Create(ctx context.Context, data entities.Data) error {
/* Create method implementation for PostgreSQL */
// code
// code
// code
return nil
}
func (pr *postgresRepository) Read(ctx context.Context, pk string) (*entities.Data, error) {
/* Read method implementation for PostgreSQL */
// code
// code
// code
return nil, nil
}
func (pr *postgresRepository) Update(ctx context.Context, pk string, newData entities.Data) error {
/* Update method implementation for PostgreSQL */
// code
// code
// code
return nil
}
func (pr *postgresRepository) Delete(ctx context.Context, pk string) error {
/* Delete method implementation for PostgreSQL */
// code
// code
// code
return nil
}
// ./solid/infrastructure/service/crud.go
package service
import (
"context"
"github.com/yescorihuela/solid/domain/entities"
"github.com/yescorihuela/solid/domain/repositories"
)
type CrudService struct {
MyPostgresRepository repositories.PostgresRepository
}
func NewCrudService(repository repositories.PostgresRepository) *CrudService {
return &CrudService{
MyPostgresRepository: repository,
}
}
func (cr *CrudService) Create(ctx context.Context, data entities.Data) error {
err := cr.MyPostgresRepository.Create(ctx, data)
if err != nil {
/* routine for handle error */
return err
}
return nil
}
I’ve been thinking that to instantiate a struct which satisfies the interface would be a good practice since I could use it as a mechanism which enforces the proper implementation of all those methods define previously within the interface. However I’ve just seen another approach on which I can see similar results but the alarms raise when I try to inject a concrete repository into a service and this repository doesn’t satisfy all the methods defined within the interface. A dilemma comes across: is it right to return a repository concrete type or should the constructor function return an specific interface type?
// ./solid/infrastructure/repositories/postgresql.go
package infrastructure
import (
"context"
"database/sql"
"github.com/yescorihuela/solid/domain"
"github.com/yescorihuela/solid/repository"
)
type postgresRepository struct {
db *sql.DB
}
func NewPostgresRepository(db *sql.DB) *postgresRepository {
return &postgresRepository{
db: db,
}
}
The truth is that dilemma shown before doesn’t affect to testability of the code. However by returning a specific interface type, I think it could be a handbrake by ensuring the proper instantiation of every single method of the interface before usage when you choose return an interface (your editor or IDE will yell at you if the methods haven’t been implemented).
Imagine the same example repository in PostgreSQL but I want to use search engine which would be ElasticSearch and I have some functional requirements:1. PostgreSQL should write, read and update the data.2. ElasticSeach should read only.
What will happen with the implementation of all those methods declared within the interface regarding to ElasticSearch? I will have to implement them even if I don’t need to use them. At this point the “Segregation Interface Principle” comes alive with a generic repository which contents interfaces with only one method defined, the example below:
// ./solid/domain/repositories/repository.go
package repositories
import (
"context"
"github.com/yescorihuela/solid/domain/entities"
)
type Creater interface {
Create(ctx context.Context, data entities.Data) error
}
type Reader interface {
Read(ctx context.Context, pk string) (*entities.Data, error)
}
type Updater interface {
Update(ctx context.Context, pk string, newData entities.Data) error
}
type Deleter interface {
Delete(ctx context.Context, pk string) error
}
Further, I define a repository for both PostgreSQL and ElasticSearch embedding methods:
// ./solid/domain/repositories/postgresql.go
package repository
type PostgresRepository interface {
Creater
Reader
Updater
Deleter
}
// ./solid/domain/repositories/elasticsearch.go
package repository
type ElasticSearchRepository interface {
Reader
}
The services which will use those interfaces respectively:
// ./solid/infrastructure/service/crud.go
package service
import (
"context"
"github.com/yescorihuela/solid/domain/entities"
"github.com/yescorihuela/solid/domain/repositories"
)
type CrudService struct {
MyPostgresRepository repositories.PostgresRepository
}
func NewCrudService(repository repositories.PostgresRepository) *CrudService {
return &CrudService{
MyPostgresRepository: repository,
}
}
func (cr *CrudService) Create(ctx context.Context, data entities.Data) error {
err := cr.MyPostgresRepository.Create(ctx, data)
if err != nil {
/* routine for handle and log error */
return err
}
return nil
}
// ./solid/infrastructure/service/search.go
package service
import (
"context"
"github.com/yescorihuela/solid/domain/entities"
"github.com/yescorihuela/solid/domain/repositories"
)
type SearchService struct {
MyElasticSearchRepository repositories.ElasticSearchRepository
}
func NewSearchService(repository repositories.ElasticSearchRepository) *SearchService {
return &SearchService{
MyElasticSearchRepository: repository,
}
}
func (ss *SearchService) Search(ctx context.Context, id string) *entities.Data{
result, err := ss.MyElasticSearchRepository.Read(ctx, id)
if err != nil {
/* routine for handle error */
return nil
}
return result
}
It’s fascinating, isn’t it? I’m astonished!
I really love this approach, today I learned something new which will allow me to think better in interfaces usage.
The main.go would look like the following below:
package main
import (
"context"
"database/sql"
"fmt"
"github.com/yescorihuela/solid/domain/entities"
"github.com/yescorihuela/solid/infrastructure/repositories"
"github.com/yescorihuela/solid/infrastructure/service"
)
func main() {
var db *sql.DB // dummy main for demonstration purposes
var es repositories.ElasticSearchDbInstance
ctx := context.TODO()
myPostgresRepository := repositories.NewPostgresRepository(db)
myElasticSearchRepository := repositories.NewElasticSearchRepository(es)
crudService := service.NewCrudService(myPostgresRepository)
searchService := service.NewSearchService(myElasticSearchRepository)
err := crudService.MyPostgresRepository.Create(ctx, entities.Data{ /* dummy data */ })
if err != nil {
/* dummy logic */
}
result, err := searchService.MyElasticSearchRepository.Read(ctx, "12345")
if err != nil {
/* dummy logic */
}
fmt.Printf("%#v\n", *result)
}
Dear reader, thanks for your time. I will be uploading more technical articles like this, please feel free to commect and correct me if I’m wrong. Don’t hesitate to be a member of my audience and I’ll keep you posted!
Top comments (0)