Go 1.18 ile gelen jenerikler sayesinde dil, katı bir yapı aracı olmaktan çıkarak daha ifade gücü yüksek ve tür güvenli bir mühendislik platformuna dönüştü. Orta ve ileri düzey geliştiriciler için bu, sadece konfor sağlamakla ilgili değil; Go'nun ünlü güçlü statik tür güvenliğini korurken tekrarlayan kodları (boilerplate) ortadan kaldırmakla da ilgilidir.
Bu yazıda, modern Go jeneriklerini kullanarak üç temel tasarım deseni olan Builder, Factory ve Strategy desenlerini nasıl uygulayacağımızı keşfedeceğiz. Bir zamanlar karmaşık arayüzler veya yansıma (reflection) gerektiren bu desenler, şimdi zarif ve derleme zamanı güvenliği ile uygulanabilir.
Jeneriklerle Builder Deseni
Builder deseni, karmaşık nesneleri adım adım oluşturmak için hayati önem taşır. Jeneriklerden önce geliştiriciler genellikle "fonksiyonel seçenekler" desenini kullanır veya tür güvenliğini feda eden boş arayüzlere (empty interfaces) dayanırdı. Jeneriklerle birlikte, builder metodlarımızın belirli bir jenerik tür parametresine sahip bir yapı (struct) döndüğünü sağlayabiliriz; bu da tür bilgisi kaybetmeden akıcı API'ler (fluent APIs) oluşturmanıza olanak tanır.
Bir veritabanı bağlantısı için yapılandırma yapısını ele alalım. Jenerik bir builder kullanarak, nihai çıktının doğru türde olduğundan emin olabiliriz.
package main
import "fmt"
// Jenerik Config yapısı
type Config[T any] struct {
Driver string
Params map[string]T
}
// Jenerik Builder
type Builder[T any] struct {
config *Config[T]
}
func NewConfigBuilder[T any]() *Builder[T] {
return &Builder[T]{
config: &Config[T]{
Params: make(map[string]T),
},
}
}
func (b *Builder[T]) Driver(d string) *Builder[T] {
b.config.Driver = d
return b
}
func (b *Builder[T]) AddParam(key string, val T) *Builder[T] {
b.config.Params[key] = val
return b
}
func (b *Builder[T]) Build() *Config[T] {
return b.config
}
// Kullanım
func main() {
// T, int olarak çıkarılır
cfg := NewConfigBuilder[int]().
Driver("postgres").
AddParam("max_connections", 100).
AddParam("timeout", 30).
Build()
fmt.Println(cfg.Params["max_connections"]) // Çıktı: 100
}
Bu yaklaşım, `AddParam` metodunun yalnızca belirtilen `T` türünü kabul ettiğini ve `interface{}` ile ilişkili çalışma zamanı paniklerini önlediğini sağlar.
Factory Deseni: Switch İfadelerinden Kurtulun
Factory deseni, nesne oluşturma işlemini soyutlar. Geleneksel Go'da bu genellikle büyük switch ifadeleri veya kayıt haritaları içerirdi. Jenerikler, sağlanan bir yapılandırıcı fonksiyona dayanarak farklı türleri anında oluşturabilen jenerik bir factory oluşturmamıza olanak tanır; bu da gevşek bağımlılığı azaltır ve test edilebilirliği artırır.
package main
import "fmt"
// Jenerik Factory türü
type Factory[T any] struct {
creators map[string]func() T
}
func NewFactory[T any]() *Factory[T] {
return &Factory[T]{
creators: make(map[string]func() T),
}
}
// Register, somut türleri veya factory fonksiyonlarını eklemeye olanak tanır
func (f *Factory[T]) Register(name string, creator func() T) {
f.creators[name] = creator
}
// Create, kayıtlı fonksiyonu kullanarak türü anında oluşturur
func (f *Factory[T]) Create(name string) (T, error) {
creator, ok := f.creators[name]
if !ok {
var t T
return t, fmt.Errorf("bilinmeyen tür: %s", name)
}
return creator(), nil
}
// Yapılarla örnek kullanım
type Animal struct {
Name string
}
func DogFactory() Animal {
return Animal{Name: "Dog"}
}
func CatFactory() Animal {
return Animal{Name: "Cat"}
}
func main() {
animalFactory := NewFactory[Animal]()
animalFactory.Register("dog", DogFactory)
animalFactory.Register("cat", CatFactory)
animal, _ := animalFactory.Create("dog")
fmt.Println(animal.Name) // Çıktı: Dog
}
Bu desen, özellikle belirli uygulamaların derleme zamanında bilinmediği ancak jenerik bir arayüze veya yapı tanımına uyması gereken eklenti mimarilerinde son derece kullanışlıdır.
Tür Kısıtlamalarıyla Strategy Deseni
Strategy deseni, bir aile algoritma tanımlar, her birini kapsüller ve bunları birbirinin yerine kullanılabilir hale getirir. Arayüzler bu amaçla Go'nun her zaman yolu olmuştur, ancak jenerikler arayüz yönlendirme (dispatch) yükünden kaçınarak ve derleyicinin daha iyi satır içleme (inlining) yapmasına izin vererek performans avantajı sunar.
Stratejimizin yalnızca belirli işlemleri destekleyen türlere uygulanmasını sağlamak için tür kısıtlamalarını kullanabiliriz.
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// NumericStrategy, herhangi bir sayısal türe bir algoritma uygular
type NumericStrategy[T constraints.Integer | constraints.Float] interface {
Process(items []T) T
}
type MaxFinder[T constraints.Integer | constraints.Float] struct{}
func (m MaxFinder[T]) Process(items []T) T {
if len(items) == 0 {
var zero T
return zero
}
max := items[0]
for _, item := range items[1:] {
if item > max {
max = item
}
}
return max
}
func main() {
strategy := MaxFinder[int]{}
result := strategy.Process([]int{1, 5, 3, 9, 2})
fmt.Println(result) // Çıktı: 9
floatStrategy := MaxFinder[float64]{}
fResult := floatStrategy.Process([]float64{1.1, 5.5, 3.3})
fmt.Println(fResult) // Çıktı: 5.5
}
Sonuç
Go'da jenerikler sihirli bir çözüm olmasa da, kod tekrarını azaltmak ve tür güvenliğini artırmak için güçlü bir araçtır. Builder, Factory ve Strategy desenlerini jeneriklerle uygulayarak, yalnızca daha performanslı değil, aynı zamanda bakımı ve testi daha kolay kodlar yazabilirsiniz. Go ekosistemi olgunlaştıkça, bu desenlerin yüksek ölçekli uygulamalarda standart uygulama haline gelmesini bekliyoruz. Jenerikleri benimseyin, ancak her zaman okunabilirliği ve sadeliği önceliklendirin.