Yıllardır Go geliştiricileri sadelik ve açık kodla övünürler. Ancak uygulamalar karmaşıklaştıkça, veritabanı işlemleri için tekrarlayan iskelet kod (boilerplate) kod tabanlarını doldurur. Go 1.18 öncesi, jenerik bir depo katmanı uygulamak ya da zahmetli arayüz jimnastiği yapmak ya da farklı varlıklar arasında mantığı kopyalayıp yapıştırmak gerekiyordu. Jeneriklerin tanıtılmasıyla Go, sadelik felsefesinden ödün vermeden sağlam tasarım desenlerini destekleyebilen bir dile dönüşmüştür.
Bu yazıda, modern Go'da Jenerik Depo deseninin nasıl uygulanacağını keşfedeceğiz. Yaygın CRUD (Oluştur, Oku, Güncelle, Sil) işlemlerini herhangi bir varlık üzerinde gerçekleştirmenize olanak tanıyan, iskelet kodu büyük ölçüde azaltırken tür güvenliğini koruyan yeniden kullanılabilir bir soyutlama oluşturacağız.
Neden Jenerik Depo Kullanılır?
Depo deseni, alan (domain) ve veri eşleme katmanları arasında bir aracı görevi görür. Birincil amacı, iş mantığını veri erişim mantığından ayırmaktır. Tipik bir Go uygulamasında, User, Post ve Comment modelleri için neredeyse aynı sorgu yapılarını yazıyor olabilirsiniz.
Jenerikler olmadan bu yeniden kullanılabilirliği elde etmek genellikle arayüzler geçirmeyi veya yansıma (reflection) kullanmayı gerektirir; ancak bunlar çalışma zamanı hatalarına veya performans yüküne yol açabilir. Jenerikler, veritabanı katmanının derleme zamanında hangi alanların mevcut olduğunu tam olarak bildiğini güvence altına alarak, herhangi bir T türüyle çalışan tek bir arayüz veya yapı tanımlamamıza olanak tanır.
Jenerik Arayüzün Tanımlanması
Temel işlemleri özetleyen jenerik bir arayüzle başlayalım. Bu arayüz, T türü tarafından parametreleştirilecektir. Bu örnekte, teorik bir ORM veya SQL sürücüsü kullandığımızı varsayacağız, ancak desen, altta yatan veri kütüphanesinden bağımsız olarak tutarlı kalır.
package repository
import (
"context"
"errors"
)
// Entity, bir ID alanına sahip jenerik bir veritabanı varlığını temsil eder.
// Gerçek bir uygulamada, daha karmaşık bir arayüz kullanabilir veya
// ORM eşlemesi için belirli etiketler gerektirebilirsiniz.
type Entity interface {
GetID() any
}
// ErrNotFound, bir varlık bulunamadığında döndürülür.
var ErrNotFound = errors.New("entity not found")
// GenericRepository, herhangi bir varlık türü T için standart CRUD işlemlerini tanımlar.
type GenericRepository[T Entity] interface {
Create(ctx context.Context, entity T) error
GetByID(ctx context.Context, id any) (T, error)
Update(ctx context.Context, entity T) error
Delete(ctx context.Context, id any) error
List(ctx context.Context) ([]T, error)
}
T'nin Entity kısıtlamasını karşılaması gerektiğine dikkat edin. Bu, deposuna geçirilen herhangi bir türün bir GetID() metoduna sahip olduğunu güvence altına alır; bu da güncelleme ve silme işlemleri için hayati önem taşır.
Somut Deponun Uygulanması
Şimdi, bu arayüzü karşılayan somut bir tür uygulayalım. Burada tam bir ORM yazmayacağız, ancak uygulamanın yapısını gösterebiliriz. Örneği veritabanı bağlantısından ziyade desene odaklamak için bir haritayı (map) sahte veri deposu olarak kullanacağız.
package repository
import (
"context"
"fmt"
"sync"
)
// MockRepository, gösterim amaçlı jenerik bir uygulamadır.
type MockRepository[T Entity] struct {
mu sync.Mutex
Store map[any]T
}
// NewMockRepository, sahte depo örneğinin yeni bir örneğini oluşturur.
func NewMockRepository[T Entity]() *MockRepository[T] {
return &MockRepository[T]{
Store: make(map[any]T),
}
}
// Create, yeni bir varlığı depoya ekler.
func (r *MockRepository[T]) Create(ctx context.Context, entity T) error {
r.mu.Lock()
defer r.mu.Unlock()
id := entity.GetID()
if _, exists := r.Store[id]; exists {
return fmt.Errorf("entity with ID %v already exists", id)
}
r.Store[id] = entity
return nil
}
// GetByID, bir varlığı kimliğine göre getirir.
func (r *MockRepository[T]) GetByID(ctx context.Context, id any) (T, error) {
r.mu.Lock()
defer r.mu.Unlock()
entity, exists := r.Store[id]
if !exists {
var zero T
return zero, ErrNotFound
}
return entity, nil
}
// List, depodaki tüm varlıkları döndürür.
func (r *MockRepository[T]) List(ctx context.Context) ([]T, error) {
r.mu.Lock()
defer r.mu.Unlock()
entities := make([]T, 0, len(r.Store))
for _, v := range r.Store {
entities = append(entities, v)
}
return entities, nil
}
MockRepository[T Entity] kullanarak, derleyicinin yalnızca Entity uygulayan türlerin bu depo ile kullanılabileceğini zorunlu kıldığını güvence altına alırız. GetID() metoduna sahip olmayan bir yapı için bir depo oluşturmaya çalışırsanız, kod derlenmez.
Pratik Kullanım Örneği
Bu desenin bir hizmet katmanında nasıl kullanılmasına bir göz atalım. Basit bir kullanıcı yapısı tanımlayalım:
type User struct {
ID int64
Name string
}
func (u User) GetID() any {
return u.ID
}
Artık kullanıcılar için özel olarak bir depo örneği oluşturabilirsiniz:
userRepo := repository.NewMockRepository[User]()
err := userRepo.Create(context.Background(), User{ID: 1, Name: "Alice"})
if err != nil {
log.Fatal(err)
}
user, err := userRepo.GetByID(context.Background(), 1)
if err == repository.ErrNotFound {
log.Println("User not found")
} else if err != nil {
log.Fatal(err)
}
fmt.Printf("Retrieved user: %+v\n", user)
Sonuç
Go'da Jenerik Depo desenlerini uygulamak, Go geliştiricilerinin sevdiği tür güvenliğinden yararlanırken veritabanı mantığını soyutlamak için güçlü bir yol sunar. Tekrarlayan kodları ortadan kaldırır, basit sahte uygulamalara izin vererek test etmeyi kolaylaştırır ve veri erişimi için net bir sözleşme sağlar.
Bu örnek bir sahte depo kullansa da, aynı ilkeler GORM, sqlx veya ent gibi kütüphanelerle entegrasyon sırasında da geçerlidir. Jenerikleri benimseyerek daha ifade gücü yüksek, bakımı kolay ve ölçeklenebilir Go uygulamaları yazabilirsiniz.