Go Dependency Injection(Bağımlılık Enjeksiyonu) Yöntemleri

Giriş:

Furkan Samaraz
6 min readMay 14, 2023

Go programlama dilinde bağımlılık enjeksiyonu, yazılımın sürdürülebilirliğini ve test edilebilirliğini artıran önemli bir tasarım prensibidir. Bu makalede, Go dilinde yaygın olarak kullanılan beş farklı bağımlılık enjeksiyonu yöntemini inceleyeceğiz: Constructor Injection, Method Injection, Interface Injection, Framework-Based Injection ve Property Injection. Bu enjeksiyonları yönetmeyi kolaylaştıracak iki(SingletonLazySingleton) design pattern konularına da değineceğiz.

1- Constructor Injection:

Constructor Injection, bir bileşenin bağımlılıklarını, bileşenin oluşturulduğu yapılandırıcı fonksiyon aracılığıyla enjekte etme yöntemidir. Örneğin, aşağıdaki kod örneğinde, ServiceB, ServiceA’ya bağımlıdır ve bu bağımlılık, ServiceB’nin yapıcı fonksiyonunda enjekte edilir.

type ServiceA struct {
//...
}

type ServiceB struct {
serviceA *ServiceA
//...
}

func NewServiceB(serviceA *ServiceA) *ServiceB {
return &ServiceB{serviceA: serviceA}
}
  • Full Code:
package main

import "fmt"

type ServiceA struct {
//...
}

type ServiceB struct {
serviceA *ServiceA
//...
}

func NewServiceB(serviceA *ServiceA) *ServiceB {
return &ServiceB{serviceA: serviceA}
}

func main() {
serviceA := &ServiceA{}
serviceB := NewServiceB(serviceA)

// Kullanım:
// serviceA ve serviceB'yi kullanarak işlemler yapılır.
fmt.Println(serviceA, serviceB)
}

2- Method Injection:

Method Injection, bir bileşenin bağımlılıklarını, bir metodun parametreleri aracılığıyla enjekte etme yöntemidir. Aşağıdaki örnekte, ServiceB’nin SomeMethod adlı bir metodu vardır ve bu metodun parametre olarak ServiceA alması gerekmektedir.

type ServiceA struct {
//...
}

type ServiceB struct {
//...
}

func (b *ServiceB) SomeMethod(serviceA *ServiceA) {
//...
}
  • Full Code:
package main

import "fmt"

type ServiceA struct {
//...
}

type ServiceB struct {
//...
}

func (b *ServiceB) SomeMethod(serviceA *ServiceA) {
//...
fmt.Println(serviceA, b)
}

func main() {
serviceA := &ServiceA{}
serviceB := &ServiceB{}
serviceB.SomeMethod(serviceA)
}

3- Interface Injection:

Interface Injection, bileşenler arasındaki bağımlılıkları bir arayüz aracılığıyla yönetme yöntemidir. Örneğin, aşağıdaki kodda ServiceB, ServiceA ile olan bağımlılığını ServiceA arayüzü üzerinden yönetir.

type ServiceA interface {
//...
}

type ServiceB struct {
serviceA ServiceA
//...
}

func NewServiceB(serviceA ServiceA) *ServiceB {
return &ServiceB{serviceA: serviceA}
}
  • Full Code:
package main

import "fmt"

type ServiceA interface {
//...
}

type ConcreteServiceA struct {
//...
}

type ServiceB struct {
serviceA ServiceA
//...
}

func NewServiceB(serviceA ServiceA) *ServiceB {
return &ServiceB{serviceA: serviceA}
}

func main() {
serviceA := &ConcreteServiceA{}
serviceB := NewServiceB(serviceA)

// Kullanım:
// serviceA ve serviceB'yi kullanarak işlemler yapılır.
fmt.Println(serviceA, serviceB)
}

4- Framework-Based Injection:

Go dilinde bağımlılık enjeksiyonunu kolaylaştırmak için bazı üçüncü taraf çerçeveler ve araçlar bulunmaktadır. Bu çerçeveler, otomatik olarak bağımlılıkları yönetmek ve enjekte etmek için yapılandırma dosyaları veya etiketler kullanır. Örneğin, popüler bir çerçeve olan “wire” ile bir örnek sunalım;

type ServiceA struct {
//...
}

type ServiceB struct {
serviceA *ServiceA
//...
}

func NewServiceB(serviceA *ServiceA) *ServiceB {
return &ServiceB{serviceA: serviceA}
}

// main.go
func InitializeServices() (*ServiceA, *ServiceB, error) {
wire.Build(NewServiceA, NewServiceB)
return nil, nil, nil
}
  • Full Code:
package main

import (
"fmt"

"github.com/google/wire"
)

type ServiceA struct {
//...
}

type ServiceB struct {
serviceA *ServiceA
//...
}

func NewServiceA() *ServiceA {
return &ServiceA{}
}

func NewServiceB(serviceA *ServiceA) *ServiceB {
return &ServiceB{serviceA: serviceA}
}

func InitializeServices() (*ServiceA, *ServiceB, error) {
wire.Build(NewServiceA, NewServiceB)
return nil, nil, nil
}

func main() {
serviceA, serviceB, err := InitializeServices()
if err != nil {
fmt.Println("Service initialization failed:", err)
return
}

// Kullanım:
// serviceA ve serviceB'yi kullanarak işlemler yapılır.
fmt.Println(serviceA, serviceB)
}

Yukarıdaki örnekte, “wire” çerçevesi kullanılarak bağımlılıklar otomatik olarak yönetilmektedir. “wire.Build” çağrısı, bağımlılıkların çözülmesi ve enjekte edilmesi için çerçeve tarafından gerçekleştirilir. Böylece, “serviceA” ve “serviceB” nesneleri initialize edilir ve kullanıma hazır hale gelir.

5- Property Injection:

Go dilinde doğrudan bir “property injection” yöntemi bulunmamaktadır. Bununla birlikte, bağımlılık enjeksiyonunu gerçekleştirmek için struct yapısı ve ilgili alanların dışarıdan atanabileceği özelliklere sahip olabilirsiniz.

Aşağıda, bir örnek ile property injection benzeri bir yaklaşıma bakacağız:

package main

import (
"fmt"
)

type Service struct {
Dependency *Dependency
}

type Dependency struct {
Name string
}

func main() {
dependency := &Dependency{Name: "Dependency"}
service := &Service{Dependency: dependency}

service.DoSomething()
}

func (s *Service) DoSomething() {
fmt.Println("Doing something with", s.Dependency.Name)
}

Yukarıdaki örnekte, Service adında bir struct ve Dependency adında bir struct tanımlanmıştır. Service struct' ının bir alanı olarak Dependency struct'ı kullanılmış ve bu alan Service'in DoSomething metodu tarafından kullanılmıştır. Örnekte, dependency değişkeni oluşturulmuş ve bu değişken Service'in Dependency alanına atanmıştır.

Böylece, Service'in Dependency alanı property injection benzeri bir şekilde dışarıdan atanmış olur. Bu sayede, Service'in bağımlılığını dışarıdan kontrol edebilir ve değiştirebilirsiniz.

Tabii ki, bu yaklaşım tam anlamıyla “property injection” olarak adlandırılmaz, ancak bağımlılığın struct alanına atanması yoluyla benzer bir etki elde edilebilir.

Not:

Property injection, diğer bağımlılık enjeksiyon yöntemleri (örneğin constructor injection) ile karşılaştırıldığında, genellikle daha az tercih edilen bir yöntemdir. Constructor injection, Go dilinde daha yaygın olarak kullanılan bir bağımlılık enjeksiyon yöntemidir.

6- Singleton Design Pattern:

Singleton tasarım deseni, bir sınıfın yalnızca bir örneğine sahip olmasını sağlar ve bu örneğe her yerden erişilebilir. İşte bir Go dilinde Singleton örneği;

package main

import "fmt"

// Dependency Interface
type DataService interface {
GetData() string
}

// Singleton Implementation
type SingletonDataService struct {
data string
}

func (s *SingletonDataService) GetData() string {
return s.data
}

var singletonInstance *SingletonDataService

func GetSingletonDataService() *SingletonDataService {
if singletonInstance == nil {
singletonInstance = &SingletonDataService{"Singleton Data"}
}
return singletonInstance
}

// Client Struct
type Client struct {
DataService DataService
}

func NewClient(dataService DataService) *Client {
return &Client{
DataService: dataService,
}
}

func main() {
singletonDataService := GetSingletonDataService()

client1 := NewClient(singletonDataService)
fmt.Println("Client 1 Data:", client1.DataService.GetData())

client2 := NewClient(singletonDataService)
fmt.Println("Client 2 Data:", client2.DataService.GetData())
}

Bu örnekte, DataService adında bir bağımlılık arayüzü tanımlanır. Ardından Singleton uygulaması olan SingletonDataService yapısı bu arayüzü uygular. GetSingletonDataService fonksiyonu, SingletonDataService örneğini oluşturur veya mevcut örneği döndürür. Client yapısı, DataService bağımlılığını alır ve bağımlılığı enjekte eder. main fonksiyonunda, SingletonDataService örneği oluşturulur ve bu örneği farklı Client'lara enjekte ederiz. Her Client'ın verileri, kullandığı SingletonDataService nesnesi tarafından sağlanır.

7- Lazy Singleton Design Pattern:

Lazy Singleton tasarım deseni, Singleton deseniyle aynı amacı taşır, ancak örneğin yaratılması sadece ihtiyaç duyulduğunda gerçekleşir. İşte bir Go dilinde Lazy Singleton örneği;

package main

import "fmt"

// Dependency Interface
type DataService interface {
GetData() string
}

// Lazy Singleton Implementation
type LazySingletonDataService struct {
data string
}

func (s *LazySingletonDataService) GetData() string {
return s.data
}

var lazySingletonInstance *LazySingletonDataService

func GetLazySingletonDataService() *LazySingletonDataService {
if lazySingletonInstance == nil {
lazySingletonInstance = &LazySingletonDataService{"Lazy Singleton Data"}
}
return lazySingletonInstance
}

// Client Struct
type Client struct {
DataService DataService
}

func NewClient(dataService DataService) *Client {
return &Client{
DataService: dataService,
}
}

func main() {
lazySingletonDataService := GetLazySingletonDataService()

client1 := NewClient(lazySingletonDataService)
fmt.Println("Client 1 Data:", client1.DataService.GetData())

client2 := NewClient(lazySingletonDataService)
fmt.Println("Client 2 Data:", client2.DataService.GetData())
}

Yukarıdaki kod örneğinde, LazySingletonDataService adında bir Lazy Singleton uygulaması tanımlanır. GetLazySingletonDataService fonksiyonu, LazySingletonDataService örneğini oluşturur veya mevcut örneği döndürür. Bu örnek, ilk kez çağırıldığında oluşturulur ve sonraki çağırılarda aynı örneği döndürür.Client yapısı, DataService bağımlılığını alır ve bağımlılığı enjekte eder.main fonksiyonunda, Lazy Singleton Dependency Injection kullanılarak LazySingletonDataService örneği oluşturulur ve bu örnek farklı Client'lara enjekte edilir.

Bu örnekte, client1 ve client2 aynı Lazy Singleton DataService örneğini paylaşırlar, çünkü aynı örneği enjekte ederler. Bu şekilde, tek bir Lazy Singleton örneği oluşturulur ve bu örnek tüm bağımlılık ihtiyaçlarını karşılar.

+++

Bu makalede, Go dilinde bağımlılık enjeksiyonunun dört farklı yöntemini inceledik. Constructor Injection, Method Injection, Interface Injection ve Framework-Based Injection yöntemlerini gördük. Her bir yöntemin kendi kullanım senaryoları ve avantajları vardır.

Constructor Injection, bileşenlerin oluşturulduğu yapılandırıcı fonksiyonlar aracılığıyla bağımlılıkları enjekte eder. Bu yöntem, bileşenler arasındaki ilişkilerin açık bir şekilde ifade edilmesini sağlar.

Method Injection, bağımlılıkların bir metot parametresi olarak enjekte edildiği bir yöntemdir. Bu yöntem, dinamik bağımlılıkların yönetilmesini ve bileşenlerin daha esnek bir şekilde kullanılmasını sağlar.

Interface Injection, bileşenler arasındaki bağımlılıkları bir arayüz üzerinden yönetir. Bu yöntem, bileşenlerin belirli bir kontratı takip etmesini ve farklı uygulamaların aynı arayüzü uygulayarak değiştirilebilir olmasını sağlar.

Framework-Based Injection, ise üçüncü taraf çerçeveler ve araçlar aracılığıyla otomatik bağımlılık enjeksiyonunu sağlar. Bu çerçeveler, yapılandırma dosyaları veya etiketler kullanarak bağımlılıkları otomatik olarak yönetir.

Property Injection, ise bir bileşenin bağımlılığı, bileşenin özellikleri (property) aracılığıyla enjekte edilir. Bağımlılık, bileşenin dışarıdan atanabilir bir özelliği olarak tanımlanır ve enjekte edilir.

Singleton design pattern, bir sınıfın yalnızca bir örneğine sahip olmasını sağlar ve bu örneğe her yerden erişilebilir. Singleton tasarım deseninde, örneğin yaratılması ilk ihtiyaç duyulduğunda gerçekleşir ve sonraki çağrılarda aynı örnek döndürülür.

Lazy Singleton design pattern, ise Singleton deseniyle aynı amacı taşır, ancak örneğin yaratılması sadece ihtiyaç duyulduğunda gerçekleşir. İlk ihtiyaç duyulduğunda örnek oluşturulur ve sonraki çağrılarda aynı örnek döndürülür.

Projenizin ihtiyaçlarına ve tercihlerinize bağlı olarak bu yöntemlerden birini veya birkaçını seçebilirsiniz. Bağımlılık enjeksiyonu, kodunuzun sürdürülebilirliğini, test edilebilirliğini ve genel olarak yazılım tasarımını iyileştiren güçlü bir araçtır.

Umarım bu makale, Go dilinde bağımlılık enjeksiyonu konusunda size yol gösterici olmuştur. Daha fazla ayrıntıya ve farklı senaryolara dalış yaparak kendi projelerinizde bu yöntemleri kullanabilirsiniz.

Teşekkür ederim ve keyifli kodlamalar dilerim!

--

--