Dependancy injection is a pattern where we pass down dependancies to initializers. This pattern is used to avoid global variables and to make code more testable.
For example in go, you can create a service
which depends on a repository
and pass the repository to the service initializer.
Suppose you have a directory like this
server
├─entities
│ ├─data.go
│ └─server.go
├─handler
│ └─handler.go
├─service
│ └─service.go
└─storage
└─storage.go
main.go
suppose you have a User
struct in data.go
package entities
type User struct {
UID string `json:"uid"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
in server.go
you can have interfaces for storage
, service
and handler
.
package entities
type IStorage interface {
CreateUserStorage(user User) err
DeleteUserStorage(uid string) err
}
type IService interface {
CreateUserService(user User) err
DeleteUserService(uid string) err
}
type IHandler interface {
CreateUserHandler(c *fiber.Ctx) err
DeleteUserHandler(c *fiber.Ctx) err
}
In storage.go
you can have a struct which implements Storage
interface.
package storage
import "app/server/entities"
type Storage struct {
db *sql.DB
}
func NewStorage(db *sql.DB) entities.IStorage {
return &Storage{
db: db,
}
}
// Implement Storage interface for example
func (s *Storage) CreateUserStorage(user entities.User) err {
panic("Not implemented")
// fmt.Println("Create user in db")
}
In service.go
you can have a struct which implements Service
interface.
package service
import "app/server/entities"
type Service struct {
storage entities.IStorage
}
func NewService(storage entities.IStorage) entities.IService {
return &Service{
storage: storage,
}
}
// Implement Service interface
func (s *Service) CreateUserService(user entities.User) err {
password := hasher.Hash(user.Password)
user.Password = password
err := s.storage.CreateUserStorage(user)
if err != nil {
return err
}
}
In handler.go
you can have a struct which implements Handler
interface.
package handler
import "app/server/entities"
type Handler struct {
service entities.IService
}
func NewHandler(service entities.IService) entities.IHandler {
return &Handler{
service: service,
}
}
// Implement Handler interface
func (h *Handler) CreateUserHandler(c *fiber.Ctx) err {
userData := new(entities.User)
// get data from request body ( json in this example )
if err := c.BodyParser(userData); err != nil {
return err
}
err := h.service.CreateUserService(userData)
if err != nil {
errMsg := handleError(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"message": errMsg,
})
}
return c.JSON(fiber.Map{
"message": "User created successfully",
})
}
In main.go
you can initialize all the dependancies and pass them down to initializers.
package main
import (
"app/server/entities"
"app/server/handler"
"app/server/service"
"app/server/storage"
)
func main() {
// Initialize db
db, err := sql.Open("sqlite3", "./db.sqlite")
if err != nil {
log.Fatal(err)
}
// Initialize storage
appStorage := storage.NewStorage(db)
appService := service.NewService(appStorage)
appHandler := handler.NewHandler(appService)
// Initialize server
app := fiber.New()
// Register routes
app.Get("/user", appHandler.CreateUserHandler)
app.Delete("/user/:uid", appHandler.DeleteUserHandler)
// Start server
app.Listen(":3000")
}
Why all the trouble ? well, this pattern makes code more testable and maintainable. You can easily mock dependancies and test each module individually.
you can use mockery to generate mocks for interfaces.
mockery --all --output ./mocks
then use the mocks in your tests.
func TestCreateUserService(t *testing.T) {
mockStorage := new(mocks.IStorage)
// mock the return value of CreateUserStorage
mockStorage.On("CreateUserStorage", mock.Anything).Return(nil)
service := NewService(mockStorage)
err := service.CreateUserService(user)
assert.Nil(t, err)
}
There you go, a clean and testable codebase.