Dependency Injection (Bağımlılık Enjeksiyonu) - (Go)

Özkan ŞEN
GoTurkiye
Published in
3 min readJul 22, 2023
Dependency Injection (DI)

Bakımı, test etmesi, genişletmesi kolay bir kod istiyorsanız Dependency Injection (DI) ihtiyacımız olan yöntem olabilir.

DI, bağımlı olduğumuz kaynakları soyutlayacak şekilde kodlama yapmaktır (function, struct metodları gibi). Bu bağımlılıklarımız soyut olduğu için, bunlarda yapılan değişiklikler kodumuzda değişiklik yapılmasını gerektirmez. Bunun için kullanılan kavram ayrıştırmadır (decoupling).

Go 'da arayüzler (interface) ve fonksiyonları için ise kapanış (closure), anonim ve değişmez (literal) için desteği vardır.

type Validator interface {
Validate(v any) error
}

type Saver interface {
Save(v any) error
}

func Execute(data []byte, s Saver, v ...Validator) error {
for _, validator := range v {
if err := validator.Validate(data); err != nil {
return err
}
}
if err := s.Save(data); err != nil {
return err
}
return nil
}

Yukarıda Validator ve Saver adında iki adet interface mevcut. Execute fonksiyonu içerisine ikiside arayüz olarak tanımlanmış. Burada Validator veya Saver ın ne olduğunu, arkada ne yaptığını veya nasıl yaptığını bilmiyoruz, aslında hiçbir önemi de yok.

Fonksiyon değişmezi (function literal) üzerinden bir örnek ile görelim:

func LoadUser(id int, decodeUser func(data []byte) *User) (*User, error) {
err := validate(id)
if err != nil {
return nil, err
}

userByte, err := getUser(id)
if err != nil {
return nil, err
}
return decodeUser(userByte), nil
}

decodeUser ise byte dizisini User objesine dönüştürür, fakat nasıl dönüştürdüğünü bilmiyoruz.

Yani burada vurgulamak istediğimiz DI 'nın ilk avantajı: bağımlılıkları soyut veya genel bir şekilde ifade ederek bir kod parçası üzerinde çalışırken gereken bilgiyi azaltır.

DI ikinci avantajı: artık bu kodlar içerisinde soyutladığımız alanları sahte kodlar ile değiştirip, dış bağımlılıklardan izole ederek sadece ilgili fonksiyonu test edebiliriz. Aşağıdaki örnek kod üzerinde görebilirsiniz.

type SaverMock struct {
retVal error
}

func (s SaverMock) Save(_ any) error {
return s.retVal
}

type ValidateMock struct {
retVal error
}

func (vm ValidateMock) Validate(_ any) error {
return vm.retVal
}

func TestExecute(t *testing.T) {
type args struct {
data []byte
s Saver
v []Validator
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "example-test-single-validate-1",
args: args{
data: []byte("test message"),
s: SaverMock{retVal: nil},
v: []Validator{ValidateMock{retVal: nil}},
},
wantErr: false,
},
{
name: "example-test-multiple-validate-1",
args: args{
data: []byte("test message"),
s: SaverMock{retVal: nil},
v: []Validator{ValidateMock{retVal: nil}, ValidateMock{retVal: nil}},
},
wantErr: false,
},
{
name: "example-fail-test-single-validate-1",
args: args{
data: []byte("test message"),
s: SaverMock{retVal: nil},
v: []Validator{ValidateMock{retVal: errors.New("error")}},
},
wantErr: true,
},
{
name: "example-fail-test-multiple-validate-1",
args: args{
data: []byte("test message"),
s: SaverMock{retVal: nil},
v: []Validator{ValidateMock{retVal: errors.New("error")}, ValidateMock{retVal: errors.New("error")}},
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Execute(tt.args.data, tt.args.s, tt.args.v...); (err != nil) != tt.wantErr {
t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

DI üçüncü avantajı ise, yukarıda gördüğünüz gibi yazılan testler hızlı, öngörülebilir ve güvenlidir.

Günün sonunda, DI, kodun anlaşılmasını, test edilmesini, genişletilmesini, yeniden kullanılmasını kolaylaştırabilen ve döngüsel bağımlılık problemlerini azaltabilen bir araçtır.

Bu yazıda daha önceden aldığım notlar ile beraber anlatımı basit ve anlaşılır tutmaya çalıştım.

Eklemek veya paylaşmak istediğiniz bir şey var ise yorum kısmını kullanabilirsiniz.

Faydalı olması dileğiyle. Başarılar...

--

--