Go 1.18'in tanıtımından bu yana Go ekosistemi önemli ölçüde gelişti. Jeneriklerin eklenmesi, Go geliştirme sürecindeki en yaygın şikayetlerden biri olan çoklu türleri işlemek için tekrarlayan kod (boilerplate) ihtiyacını giderdi. Temiz, DRY (Kendini Tekrar Etme) ve sürdürülebilir kod yazmak isteyen orta ve ileri düzey geliştiriciler için jeneriklerden yararlanmak artık bir seçenek değil, bir gerekliliktir.
Bu yazıda, Go jeneriklerini kullanarak sağlam ve tür güvenli bir Repository Deseni'nin nasıl uygulanacağını keşfedeceğiz. Bu desen, veritabanı soyutlaması için hayati öneme sahiptir ve iş mantığının belirli veritabanı uygulamalarından bağımsız kalmasını sağlarken, derleme zamanında tür güvenliğini garanti eder.
Neden Go'da Repository Deseni?
Repository Deseni, alan (domain) ve veri eşleme katmanları arasında bir aracı görevi görür ve alan nesnelerine erişim için koleksiyon benzeri bir arayüz sağlar. Jenerikler olmadan geleneksel Go geliştirme sürecinde, her varlık (Kullanıcı, Ürün, Sipariş) için bir repository uygulamak, her veritabanı işlemi (Oluştur, Oku, Güncelle, Sil) için tekrarlayan kod yazmayı gerektirirdi.
Jenerikleri kullanarak, farklı varlık türleri arasında çalışan tek bir Repository arayüzü ve uygulaması tanımlayabilir, kod tekrarını azaltabilir ve sürdürülebilirliği artırabiliriz.
Jenerik Repository Arayüzünün Tanımlanması
Çözümümüzün özü, jenerik bir arayüz tanımlamaktadır. Varlık türünü temsil etmek için bir tür parametresi T kullanırız. Ayrıca, yalnızca belirli alanlara (bir ID gibi) sahip olan yapıların varlık olarak kullanılabileceğini zorlamak için bir kısıtlama arayüzü Entity tanıtıyoruz.
package repository
import "context"
// EntityConstraint, yalnızca ID alanına sahip yapıların varlık olarak kullanılabileceğini sağlar.
type EntityConstraint interface {
~struct{
ID int64
}
}
// Repository, veritabanı işlemleri için jenerik arayüzü tanımlar.
type Repository[T any] interface {
Create(ctx context.Context, entity *T) error
Read(ctx context.Context, id int64) (*T, error)
Update(ctx context.Context, entity *T) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context) ([]*T, error)
}
Burada any herhangi bir türü kabul eder, ancak pratikte yapısal kuralları zorlamanız gerekiyorsa T'yi EntityConstraint arayüzünü uygulayacak şekilde kısıtlayabilirsiniz. Yöntemler *T kabul eder, böylece çağrıyanın belirli varlık türüne işaretçiler sağladığından emin olunur.
Jenerik Repository'nin Uygulanması
Yapıların içine tarama (scanning) desteği sunan popüler bir SQL kütüphanesi olan sqlx kullanarak somut bir uygulama oluşturalım. Bu uygulama, jeneriklerin birden fazla tablo türü için tek bir işleyici yazmamıza nasıl olanak sağladığını gösterir.
package repository
import (
"context"
"database/sql"
"fmt"
)
type GenericRepository[T any] struct {
db *sql.DB
tableName string
}
func NewGenericRepository[T any](db *sql.DB, tableName string) *GenericRepository[T] {
return &GenericRepository[T]{
db: db,
tableName: tableName,
}
}
func (r *GenericRepository[T]) Create(ctx context.Context, entity *T) error {
// Gerçek dünya senaryolarında, yapı alanlarına dayalı olarak INSERT ifadesini dinamik olarak oluşturmak için
// yansıma (reflection) veya makro ile oluşturulmuş bir sorgu kullanırsınız.
// Kısalık adına, sorguyu oluşturduğu varsayılan bir yardımcı işlevin var olduğunu varsayıyoruz.
query := fmt.Sprintf("INSERT INTO %s VALUES (?, ?, ?)", r.tableName)
_, err := r.db.ExecContext(ctx, query, /* entity'den alanlar */ )
return err
}
func (r *GenericRepository[T]) Read(ctx context.Context, id int64) (*T, error) {
var entity T
query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", r.tableName)
err := r.db.QueryRowContext(ctx, query, id).Scan(&entity)
if err != nil {
return nil, err
}
return &entity, nil
}
Read yönteminin *T döndürdüğüne dikkat edin. Derleyici, bu yöntemi çağırdığınızda döndürülen değişkenin, repository'yi başlattığınız türle eşleştiğini garanti eder. Bu, hata yapmaya yatkın ve yavaş olan tür atamalarını (entity.(*User)) yapma ihtiyacını ortadan kaldırır.
Repository'nin Hizmet Katmanında Kullanımı
Bu desenin gerçek gücü, hizmet katmanında kullanıldığında görünür. Burada, altta yatan SQL ayrıntılarını bilmeden jenerik bir repository enjekte edebilir ve işlemler gerçekleştirebiliriz.
type UserService struct {
userRepo repository.Repository[*User]
}
func NewUserService(userRepo repository.Repository[*User]) *UserService {
return &UserService{userRepo: userRepo}
}
func (s *UserService) GetUserByID(ctx context.Context, id int64) (*User, error) {
return s.userRepo.Read(ctx, id)
}
Bu örnekte, userRepo Repository[*User] olarak türleştirilmiştir. Bu, güçlü derleme zamanı garantileri sağlar. Yanlışlıkla bu kullanıcıya özel hizmete bir *Product geçmeye çalışırsanız, Go derleyicisi bir hata atarak çalışma zamanı hatalarının önüne geçer.
En İyi Uygulamalar ve Dikkat Edilmesi Gerekenler
- Aşırı Soyutlamadan Kaçının: Jenerikler karmaşıklık ekler. Bunları yalnızca mantığı farklı türler arasında gerçekten yeniden kullanmanız gerektiğinde kullanın. Basit uygulamalar için somut uygulamalar daha net olabilir.
- Performans: Jenerikler kod tekrarını azaltsa da, jenerik işlevlerinizin derleyici tarafından satır içi (inline) hale getirildiğinden emin olun. Jenerik yöntemler içinde aşırı yansıma (reflection) kullanımından kaçının, çünkü bu performansı etkileyebilir.
- Hata Yönetimi: Jenerik işlevlerdeki hata yönetiminin sağlam olduğundan emin olun. Tür
Tderleme zamanında bilinmediği için, türe özgü hata türlerine güvenemezsiniz.
Sonuç
Go jeneriklerini ustalıkla kullanmak, ölçeklenebilir ve sürdürülebilir uygulamalar oluşturma konusunda yeni olanaklar sunar. Jenerik bir Repository Deseni uygulayarak, Go'nun bilinen tür güvenliğini korurken yüksek derecede kod yeniden kullanımına ulaşabilirsiniz. Go'da geliştirmeye devam ettikçe, jenerikleri yalnızca bir özellik olarak değil, temiz mimariler tasarlamak için temel bir araç olarak benimseyin.
Tekrarlayan veritabanı işleyicilerinizi bugün yeniden düzenlemeye başlayın ve tür güvenli, jenerik kodun faydalarını deneyimleyin.