Inversion of Control

Introduction to IoC

The main goal of Inversion of Control and Dependency Injection is to remove dependencies of an application. This makes the system more decoupled and maintainable.

Dependency injection is a concept valid for any programming language. The general concept behind dependency injection is called Inversion of Control. According to this concept, a struct should not configure its dependencies statically but should be configured from the outside.

Dependency Injection design pattern allows us to remove the hard-coded dependencies and make our application loosely coupled, extendable and maintainable.

Dependency Injection

One of the most significant features of Hiboot is Dependency Injection. Hiboot implements the JSR-330 standard.

Dependency Injection provides objects that an object needs. Rather than the dependencies constructing themselves, they are injected by some external means.

Basic Example

package service

import "github.com/hidevopsio/hiboot/pkg/app"

type UserRepository interface {
	FindById(id uint64) (*User, error)
}

type UserService struct {
	repository UserRepository
}

func init() {
	app.Register(newUserService)
}

func newUserService(repository UserRepository) *UserService {
	return &UserService{
		repository: repository,
	}
}

func (s *UserService) GetUser(id uint64) (*User, error) {
	return s.repository.FindById(id)
}

Constructor Injection is the recommended approach in Hiboot. It has several advantages:

  • Testable: Easy to implement unit tests with mocks
  • Syntax validation: IDEs can validate types and avoid typos
  • Immutability: Dependencies cannot be changed after construction
  • Completeness: Ensures all required dependencies are set

Example

package controller

import (
	"github.com/hidevopsio/hiboot/pkg/app"
	"github.com/hidevopsio/hiboot/pkg/at"
	"github.com/hidevopsio/hiboot/pkg/model"

	"myapp/service"
)

type userController struct {
	at.RestController
	at.RequestMapping `value:"/user"`

	userService *service.UserService
}

func init() {
	app.Register(newUserController)
}

// Dependencies are injected through constructor arguments
func newUserController(userService *service.UserService) *userController {
	return &userController{
		userService: userService,
	}
}

func (c *userController) GetById(id uint64) (model.Response, error) {
	user, err := c.userService.GetUser(id)
	response := new(model.BaseResponse)
	response.SetData(user)
	return response, err
}

Field Injection

Field Injection is triggered by the inject:"" struct tag. When this tag is present on a field, Hiboot tries to resolve the object to inject by the type of the field.

Basic Field Injection

type userController struct {
	at.RestController

	UserService *service.UserService `inject:""`
}

func newUserController() *userController {
	return &userController{}
}

func init() {
	app.Register(newUserController)
}

Named Injection

If multiple implementations of the same interface are available, disambiguate using the field name or tag value:

type AuthService interface {
	Authenticate(username, password string) bool
}

// Two implementations of AuthService
type basicAuthService struct{}
type oauth2AuthService struct{}

type authController struct {
	at.RestController

	// Inject by field name (matches "basicAuthService")
	BasicAuthService AuthService `inject:""`

	// Inject by field name (matches "oauth2AuthService")
	Oauth2AuthService AuthService `inject:""`

	// Inject by explicit name
	PrimaryAuth AuthService `inject:"basicAuthService"`
}

Value Injection

Inject configuration values using the value:"" tag:

type MyService struct {
	AppName  string `value:"${app.name}"`
	Port     int    `value:"${server.port}"`
	LogLevel string `value:"${logging.level:info}"` // with default value
}

Default Value Injection

Use the default:"" tag to specify default values:

type Config struct {
	Timeout  int    `default:"30"`
	Host     string `default:"localhost"`
	Enabled  bool   `default:"true"`
}

Method Injection

Method Injection is used in auto-configuration. Dependencies are injected through method arguments:

package myconfig

import "github.com/hidevopsio/hiboot/pkg/app"

type configuration struct {
	app.Configuration
}

func init() {
	app.Register(newConfiguration)
}

func newConfiguration() *configuration {
	return &configuration{}
}

// Database is injected through method arguments
func (c *configuration) UserRepository(db *Database) UserRepository {
	return &mysqlUserRepository{db: db}
}

// UserService depends on UserRepository
func (c *configuration) UserService(repo UserRepository) *UserService {
	return &UserService{repository: repo}
}

Registration

All injectable components must be registered using app.Register():

func init() {
	// Register constructors
	app.Register(newUserService)
	app.Register(newUserController)

	// Register multiple at once
	app.Register(newFoo, newBar, newBaz)
}

Injection Rules

  1. Pointer types: Dependencies must be pointer types or interfaces
  2. Registration required: Components must be registered via app.Register()
  3. No circular dependencies: Avoid circular dependency chains
  4. Interface resolution: If multiple implementations exist, use naming to disambiguate

Valid

type Foo struct{}
type Bar struct {
	foo *Foo // pointer - will be injected
}

Invalid

type Foo struct{}
type Bar struct {
	foo Foo // not a pointer - will NOT be injected
}

Testing with Dependency Injection

Constructor injection makes testing easy:

package controller

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

// Mock implementation
type mockUserService struct {
	mock.Mock
}

func (m *mockUserService) GetUser(id uint64) (*User, error) {
	args := m.Called(id)
	return args.Get(0).(*User), args.Error(1)
}

func TestUserController_GetById(t *testing.T) {
	// Create mock
	mockService := new(mockUserService)
	mockService.On("GetUser", uint64(1)).Return(&User{Name: "Test"}, nil)

	// Inject mock through constructor
	controller := newUserController(mockService)

	// Test
	response, err := controller.GetById(1)
	assert.NoError(t, err)
	assert.NotNil(t, response)

	mockService.AssertExpectations(t)
}

What’s Next?