Dependency Injunction


not done 
path includes Design Patterns/Dependency Injunction

use a container for managing objects ?
Constructor Injection and Setter Injection ?

✅ Benefits:

Helps you to decouple the external logic of your implementation


An important point of injecting dependencies is to avoid injecting implementations (structs), you should inject abstractions (interfaces). ?

An important point of injecting dependencies is to avoid injecting implementations (structs), you should inject abstractions (interfaces). It’s the letter D of S.O.L.I.D: Dependency Inversion Principle. It allows you to switch easily the implementation of some dependency and, you could change the real implementation for a mock implementation. It's fundamental for unit testing. - Source

Example of Constructor-based Dependency Injection

type SomeRepo interface{}
type SomeLogger interface{}
type SomeMessageBroker interface{}

type SomeService struct {
	repo SomeRepo
	logger SomeLogger 
	broker SomeMessageBroker 
}

func NewSomeService ( repo SomeRepo , logger SomeLogger , broker SomeMessageBroker)SomeService{
	return &SomeService{
		repo: repo,
		logger: logger,
		broker: broker,
	}
} 

In this , the NewSomeService requires (dependencies) repo, logger, broker return an instance of SomeService , dependencies are all passed in via interfaces, which is a common and idiomatic way to do DI in Go. ??

Types

  1. Constructor Injection
    • Dependencies are passed into the constructor.
    • Promotes immutability and makes the code more testable.
  2. Setter Injection
    • Dependencies are set via exported methods after the object is created.
  3. Interface Injection
    • Not common in Go. An external component defines the injection logic, more common in other languages like Java.
graph LR 
DI --> Contructor & Property & MethodsorSetter 

Constructor Injunction

func NewStaffProfileService(
	staffProfileRepository repository.StaffProfileRepository,
	staffRepo repository.StaffRepository,
	staffDetailRepo repository.StaffDetailRepository,
	staffAdditionalDetailRepo repository.StaffAdditionalDetailRepository,
	staffPayScaleRepo repository.StaffPayScaleRepository,
	serviceBreakRepo repository.StaffServiceBreakRepository,
) StaffProfileService {
	return &staffProfileService{
		staffProfileRepository:    staffProfileRepository,
		staffRepo:                 staffRepo,
		staffPayScaleRepo:         staffPayScaleRepo,
		staffDetailRepo:           staffDetailRepo,
		staffAdditionalDetailRepo: staffAdditionalDetailRepo,
		serviceBreakRepo:          serviceBreakRepo,
	}
}

These kinds allow you to change dependencies in runtime, so by design, they aren’t immutable. But if you need to change the implementation of some dependency, you don’t need to recreate everything. You can just override what you need. It may be useful if you have a feature flag that changes an implementation inside your service. - Source

Method Injunction

type staffProfileService struct {
	staffProfileRepository    repository.StaffProfileRepository
	staffRepo                 repository.StaffRepository
	staffDetailRepo           repository.StaffDetailRepository
	staffAdditionalDetailRepo repository.StaffAdditionalDetailRepository
	staffPayScaleRepo         repository.StaffPayScaleRepository
	serviceBreakRepo repository.StaffServiceBreakRepository
}

How?

Using dependency injection container.

Manually
Manually construction is an objective way to do it. You declare, create, and inject your dependencies step by step. I think it’s clean and there isn’t any magic happening behind the scenes. The problem is as your dependencies get complex you need to deal with complexity by yourself. You may see your func main() getting with hundreds of lines of code and harder to maintain. - Source

Eg:

type staffProfileService struct {
	staffRepo                 repository.StaffRepository
	staffDetailRepo           repository.StaffDetailRepository

}
func NewStaffProfileService(
	staffRepo repository.StaffRepository,
	staffDetailRepo repository.StaffDetailRepository,
) StaffProfileService {
	return &staffProfileService{
		staffRepo:                 staffRepo,
		staffPayScaleRepo:         staffPayScaleRepo,
	}
}

Implementation

Injection During Initialization

type staffAdditionalDetailRepository struct {
	db *gorm.DB
	ctx *gin.Context
}
func NewStaffAdditionalDetailRepository(db *gorm.DB, ctx *gin.Context) StaffAdditionalDetailRepository {
	return &staffAdditionalDetailRepository{db: db, ctx: ctx}
}

In this , the staffAdditionalDetailRepository depentds on db and ctx which is injected via the NewStaffAdditionalDetailRepository() function .

Property Injection

Dependencies are set after the object is created

type staffAdditionalDetailRepository struct {
	db *gorm.DB
	ctx *gin.Context
}
func(r *staffAdditionalDetailRepository) SetDB (db *gorm.DB) {
	r.db = db
}
func(r *staffAdditionalDetailRepository) SetCtx (ctx *gin.Context) {
	r.ctx = ctx 
}
	

Interface Injection (aka method injection.)

the dependency is represented by an interface that the object depends on

The dependency is then set by providing the implementation of that interface

type StaffAdditionalDetailRepository interface {
	Create(tx *gorm.DB, staffAdditionalDetail *model.StaffAdditionalDetail) error
	Delete(id int) error
}

Here the StaffAdditionalDetailRepository requires , methods Create() and Delete() to be implemented