Dağıtık sistemler dünyasında, mikroservisler arasındaki doğrudan nokta-nokta iletişim genellikle "spagetti" bir mimariye yol açar. Servis sayısı arttıkça, bağımlılıkları yönetmenin karmaşıklığı doğrusal olmayan bir şekilde yükselir. Buna karşı mimarlar genellikle Mediator Deseni'ne başvurur; bu, davranışsal bir tasarım desenidir ve nesnelerin birbirlerine açıkça başvurmasını engelleyerek gevşek bağlamayı (coupling) azaltır. Bu yazıda, temiz, sürdürülebilir ve ölçeklenebilir bir mikroservis iletişim katmanı oluşturmak için bu deseni Go'da nasıl etkili bir şekilde uygulayacağımızı keşfedeceğiz.
Sıkı Bağlamanın (Tight Coupling) Sorunu
Service A'nın Service B, C ve D ile iletişim kurması gereken bir senaryoyu düşünün. Bir aracı (mediator) olmadan, Service A'nın aşağıdaki tüm servislere ait ağ adreslerini, protokolleri ve hata yönetimi stratejilerini bilmesi gerekir. Bu, yüksek bir bağlama derecesi yaratır. Service B API'sini değiştirirse, Service A'nın güncellenmesi, test edilmesi ve yeniden dağıtılması gerekir. Bu sıkı bağlama, sistemi kırılgan hale getirir ve ölçeklendirmeyi zorlaştırır. Mediator Deseni, tüm etkileşimleri yöneten bir ara nesne tanıtarak bu sorunu çözer. Artık Service A'nın, bireysel servislere değil, yalnızca Mediator ile nasıl iletişim kuracağını bilmesi yeterlidir.
Go'da Mediator'un Uygulanması
Go, karmaşık kalıtım hiyerarşileri yerine sadeliği ve bileşenleri bir araya getirmeyi (composition) teşvik eder. Mediator Deseni'ni arayüzler ve struct'lar kullanarak uygulayabiliriz. Temel bileşenler Mediator arayüzü, Colleague arayüzü (servisleri temsil eder) ve somut Mediator uygulamasıdır.
// ColleagueInterface, aracıya katılan tüm servisler için sözleşmeyi tanımlar.
type ColleagueInterface interface {
Notify(event string, data interface{})
}
// MediatorInterface, aracı için sözleşmeyi tanımlar.
type MediatorInterface interface {
Send(message string, colleague ColleagueInterface)
Register(colleague ColleagueInterface)
}
// ConcreteMediator, arkadaşlar (colleagues) arasındaki iletişimi koordine eder.
type ConcreteMediator struct {
colleagues map[string]ColleagueInterface
}
func NewConcreteMediator() *ConcreteMediator {
return &ConcreteMediator{
colleagues: make(map[string]ColleagueInterface),
}
}
func (m *ConcreteMediator) Register(c ColleagueInterface) {
// Gerçek bir senaryoda, servisleri otomatik olarak algılamak ve kaydetmek
// için yansıma (reflection) veya bir kayıt defteri kullanabilirsiniz.
if col, ok := c.(interface{ GetName() string }); ok {
m.colleagues[col.GetName()] = c
}
}
func (m *ConcreteMediator) Send(message string, colleague ColleagueInterface) {
targetName := colleague.GetName()
if target, exists := m.colleagues[targetName]; exists {
// Bir mesaj göndermeyi simüle edin (örn. gRPC veya Kafka üzerinden)
fmt.Printf("Mediator '%s' mesajını %s adresine gönderiyor\n", message, targetName)
target.Notify("received", message)
} else {
fmt.Println("Hedef arkadaş bulunamadı.")
}
}
Pratik Örnek: Sipariş İşleme İş Akışı
Bunu pratik bir örneğe uygulayalım: Bir Sipariş İşleme Sistemi. OrderService'un bir InventoryService ve bir PaymentService'i bildirmesi gerekir. OrderService'in her ikisine de referans tutması yerine, Mediator'u kullanır.
// OrderService bir colleague olarak hareket eder
type OrderService struct {
name string
mediator MediatorInterface
}
func NewOrderService(m MediatorInterface) *OrderService {
return &OrderService{
name: "OrderService",
mediator: m,
}
}
func (o *OrderService) GetName() string { return o.name }
func (o *OrderService) PlaceOrder() {
fmt.Println("Sipariş verildi. Mediator'lara bildiriliyor...")
o.mediator.Send("order_created", o)
}
// PaymentService başka bir colleague olarak hareket eder
type PaymentService struct {
name string
}
func (p *PaymentService) GetName() string { return p.name }
func (p *PaymentService) Notify(event string, data interface{}) {
fmt.Printf("PaymentService şu olayı aldı: %s\n", event)
}
main işlevinde, bu servisleri aracıya kaydederiz. OrderService bir sipariş verdiğinde, Mediator'un ilgili dinleyicilere dağıttığı bir bildirimi tetikler. Bu yaklaşım, OrderService'in kaç tane ödeme kapısı veya envanter sistemi bulunduğundan habersiz kalmasını sağlar; yalnızca eylemlerin aracıya iletilmesini isteyebileceğini bilir.
Faydalar ve Takaslar (Trade-offs)
Go'da Mediator Deseni'ni benimsemek önemli faydalar sağlar. Karmaşık iletişim mantığını yerelleştirerek kodu basitleştirir ve değiştirmeyi veya genişletmeyi kolaylaştırır. Mevcut olanlara dokunmadan yeni servisler ekleyebilirsiniz; bu da Açık/Kapalı İlkesine (Open/Closed Principle) uymayı sağlar. Ancak, takaslar da vardır. Dikkatli tasarlanmazsa, Mediator kendisi bir "tanrı nesnesi" (god object) haline gelebilir ve çok fazla sorumluluk biriktirebilir. Mediator'u hafif tutmak ve sistem çok büyürse onu birden fazla aracıya bölmeyi düşünmek önemlidir. Ayrıca, yürütme akışı dolaylı olduğu için hata ayıklama biraz daha zorlaşabilir. Bunu azaltmak için yapılandırılmış günlükleme (structured logging) ve izleme kimlikleri (tracing IDs) kullanmak esastır.
Sonuç
Mediator Deseni, Go mikroservislerinde karmaşıklığı yönetmek için güçlü bir araçtır. İletişim mantığını merkezileştirerek gevşek bağlamayı teşvik eder ve sürdürülebilirliği artırır. Koordinasyon için merkezi bir nokta tanıtsa da, dikkatli tasarım bu noktanın esnek ve ölçeklenebilir kalmasını sağlar. Düğümlü servis bağımlılıklarını yeniden düzenlemek isteyen ara ve ileri düzey geliştiriciler için, Mediator Deseni'ni uygulamak daha sağlam bir mimariye doğru stratejik bir adımdır.