Mediator Pattern ve Domain-Driven Design: Hangi problemi çözerler, birlikte nasıl kullanılırlar?
Biri kodun nasıl haberleştiğiyle, diğeri kodun neyi temsil ettiğiyle ilgilenir. Mediator pattern ile DDD'nin çözdüğü problemleri, farklarını, yaygın hataları ve birlikte kullanımını C# örnekleriyle karşılaştırdım.
Kurumsal projelerde iki kavram sık sık aynı cümlede geçer: Mediator pattern ve Domain-Driven Design (DDD). Hatta bazı ekiplerde "projeye MediatR ekledik, artık DDD yapıyoruz" cümlesini bile duyabilirsiniz. Oysa bu ikisi farklı seviyelerde, farklı problemlere çözüm üretir. Bu yazıda her birinin hangi derde deva olduğunu, nerede birbirine değdiğini ve nerede karıştırıldığını anlatmaya çalışacağım.
Kısa cevap
- Mediator bir tasarım kalıbıdır: Nesnelerin birbirini doğrudan çağırmasını engelleyip aradaki iletişimi tek bir aracıya toplar. Problemi teknik bağımlılıktır.
- DDD bir yazılım geliştirme yaklaşımıdır: Karmaşık iş kurallarını kodun merkezine koyar ve kodu işin diliyle modellemeyi hedefler. Problemi iş karmaşıklığıdır.
Biri kodun nasıl haberleştiğiyle, diğeri kodun neyi temsil ettiğiyle ilgilenir.
1. Mediator pattern: Herkes herkesi tanımasın
Gang of Four'un davranışsal kalıplarından biri olan Mediator'ün çıkış noktası basittir: Birbirini doğrudan çağıran on nesne, potansiyel olarak 45 ayrı bağımlılık demektir. Birini değiştirdiğinizde diğerlerinin kırılıp kırılmayacağını kestirmek zorlaşır.
Mediator bu ağı bir yıldıza çevirir: Nesneler birbirini değil, yalnızca aracıyı tanır.
Çözdüğü problemler
- Sıkı bağımlılık: Controller'ın onlarca servisi constructor'dan alması, servislerin birbirini çağırması.
- Şişmiş servis sınıfları:
SiparisServiceiçinde 40 metot. Mediator ile her iş tek bir handler'a ayrılır. - Ortak davranışların tekrarı: Loglama, doğrulama, transaction, yetki kontrolü gibi işlemler her handler'ın önüne bir "pipeline" adımı olarak tek yerden eklenebilir.
Basit bir örnek
// İstek ve cevabı
public record SiparisOlustur(Guid MusteriId, List<SatirDto> Satirlar) : IRequest<Guid>;
// İşin tamamı tek bir handler'da
public class SiparisOlusturHandler(ISiparisRepository repo) : IRequestHandler<SiparisOlustur, Guid>
{
public async Task<Guid> Handle(SiparisOlustur komut, CancellationToken ct)
{
var siparis = Siparis.Olustur(komut.MusteriId);
foreach (var s in komut.Satirlar) siparis.UrunEkle(s.UrunId, s.Adet, s.BirimFiyat);
await repo.EkleAsync(siparis, ct);
return siparis.Id;
}
}
// Controller artık sadece aracıyı tanıyor
app.MapPost("/siparisler", async (SiparisOlustur komut, IMediator mediator) =>
Results.Ok(await mediator.Send(komut)));
.NET dünyasında bunun en bilinen uygulaması MediatR kütüphanesi. Bir not: MediatR 2025'te ticari lisans modeline geçti. Yeni projelerde lisans koşullarına bakmakta, gerekirse açık kaynak alternatiflere ya da birkaç satırlık kendi aracınıza yönelmekte fayda var. Kalıbın kendisi bir kütüphaneye bağlı değil.
Dikkat edilmesi gerekenler
- Akış görünmez olabilir:
mediator.Send(...)satırından hangi handler'ın çalışacağını IDE'de "Go to definition" ile bulamazsınız. İsimlendirme ve klasör düzeni bu yüzden çok önemli. - Handler'dan handler çağırmak: Bir handler'ın içinden başka bir
Sendyapmak, kaçtığınız karmaşık bağımlılık ağını gizli şekilde geri getirir. - Basit CRUD için fazlalık: Üç tablolu bir yönetim paneline mediator eklemek, çözülecek bir problem olmadan katman eklemektir.
2. Domain-Driven Design: Kodu işin diliyle yazmak
Eric Evans'ın 2003'te yayımlanan kitabıyla tanımlanan DDD'nin derdi teknik değil, iş karmaşıklığıdır. Sigorta, lojistik, perakende ya da ERP gibi alanlarda asıl zorluk veritabanı ya da framework değil; kuralların kendisidir. DDD bu kuralları dağınık if bloklarından kurtarıp modelin merkezine koymayı amaçlar.
Stratejik tasarım: Büyük resim
- Ortak dil (Ubiquitous Language): İş biriminin "iade", "sevkiyat", "rezerv stok" dediği kavramlar kodda da aynı adla yer alır. Toplantıdaki cümle ile koddaki sınıf adı aynı olmalıdır.
- Sınırlı bağlam (Bounded Context): "Ürün" kavramı katalog ekibi için açıklama ve görseldir, depo için raf ve adettir, muhasebe için maliyettir. Her bağlam kendi modelini kurar; tek, devasa bir
Urunsınıfı herkesi memnun etmeye çalışmaz. - Bağlam haritası (Context Map): Bağlamların birbiriyle nasıl konuştuğunu, kimin kime bağımlı olduğunu gösterir.
Taktik tasarım: Kod seviyesi
- Entity: Kimliği olan nesne (Sipariş, Müşteri).
- Value Object: Kimliği olmayan, değeriyle tanımlanan nesne (Para, Adres). Değişmezdir.
- Aggregate: Birlikte tutarlı kalması gereken nesneler kümesi. Dışarıdan yalnızca kök (aggregate root) üzerinden değiştirilir.
- Domain Event: İş açısından anlamlı bir olayın kaydı ("Sipariş onaylandı").
- Repository: Aggregate'leri saklayıp geri getiren soyutlama.
Örnek: Kuralı modelin içine koymak
public sealed record Para(decimal Tutar, string ParaBirimi)
{
public static Para operator +(Para a, Para b) =>
a.ParaBirimi == b.ParaBirimi ? a with { Tutar = a.Tutar + b.Tutar }
: throw new InvalidOperationException("Farklı para birimleri toplanamaz.");
}
public class Siparis : AggregateRoot
{
private readonly List<SiparisSatiri> _satirlar = [];
public IReadOnlyList<SiparisSatiri> Satirlar => _satirlar;
public SiparisDurumu Durum { get; private set; } = SiparisDurumu.Taslak;
public void UrunEkle(Guid urunId, int adet, Para birimFiyat)
{
if (Durum != SiparisDurumu.Taslak)
throw new DomainException("Onaylanmış siparişe ürün eklenemez.");
if (adet <= 0)
throw new DomainException("Adet sıfırdan büyük olmalı.");
_satirlar.Add(new SiparisSatiri(urunId, adet, birimFiyat));
}
public void Onayla()
{
if (_satirlar.Count == 0)
throw new DomainException("Boş sipariş onaylanamaz.");
Durum = SiparisDurumu.Onaylandi;
OlayEkle(new SiparisOnaylandi(Id));
}
}
Bu sınıfa dışarıdan siparis.Durum = ... diye müdahale edilemez; "onaylanmış siparişe ürün eklenmez" kuralı ise uygulamanın neresinden gelinirse gelinsin korunur. Kuralların servis katmanlarına dağıldığı, entity'lerin yalnızca getter-setter'dan ibaret olduğu yapıya ise anemic domain model denir ve DDD'nin en çok kaçındığı durumdur.
Dikkat edilmesi gerekenler
- Basit iş için ağır yaklaşım: Kuralı az, ağırlıklı olarak veri girişi olan bir uygulamada aggregate, value object ve domain event kurmak gereksiz maliyettir.
- Sadece taktik kısmı almak: Sınıf adlarını
Aggregate,ValueObjectdiye değiştirip iş birimiyle hiç konuşmamak DDD değildir. DDD'nin asıl değeri stratejik tarafta, yani işi doğru anlamaktadır. - Öğrenme eğrisi: Ekipteki herkesin aynı kavramları aynı şekilde anlaması zaman alır.
3. Karşılaştırma
| Mediator Pattern | Domain-Driven Design | |
|---|---|---|
| Nedir? | Tasarım kalıbı | Yazılım geliştirme yaklaşımı |
| Seviye | Kod / uygulama katmanı | Mimari, ekip ve iş birimiyle iletişim |
| Çözdüğü problem | Bileşenler arası sıkı bağımlılık, şişmiş servisler, tekrar eden ortak davranışlar | Karmaşık iş kuralları, iş ve yazılım arasındaki dil uçurumu, büyüyen modeldeki kavram karmaşası |
| Odak | Nesneler nasıl haberleşir? | Kod işi doğru temsil ediyor mu? |
| Uygulama süresi | Birkaç saat | Projeye yayılan bir süreç |
| Tek başına kullanılır mı? | Evet, DDD olmadan da | Evet, mediator olmadan da |
| Yanlış kullanımda | Görünmez akış, gizli bağımlılıklar | Gereksiz karmaşıklık, "kâğıt üstünde DDD" |
| Ne zaman uygun? | Çok sayıda use-case, ortak pipeline ihtiyacı | Kuralları yoğun, uzun ömürlü, iş açısından kritik alanlar |
4. Birlikte nasıl kullanılırlar?
İkisi rakip değildir; iyi bir projede genellikle farklı katmanlarda yan yana dururlar:
- Uygulama katmanı (Application): Kullanıcıdan gelen her istek bir komut (command) ya da sorgu (query) olarak mediator üzerinden ilgili handler'a gider. Bu, CQRS ile de çok iyi örtüşür.
- Handler: İş kuralı yazmaz. Aggregate'i repository'den yükler, onun metodunu çağırır, sonucu kaydeder. Yani handler bir orkestra şefidir, çalgıyı çalan domain modelidir.
- Domain katmanı: Kurallar aggregate'in içindedir (
siparis.Onayla()). - Domain event'ler: Aggregate'in ürettiği olaylar ("SiparisOnaylandi") kayıt sonrası mediator'ün notification mekanizmasıyla yayımlanır; stok rezervasyonu, e-posta gönderimi gibi yan işler ayrı handler'larda yürür.
public class SiparisOnaylaHandler(ISiparisRepository repo, IUnitOfWork uow) : IRequestHandler<SiparisOnayla>
{
public async Task Handle(SiparisOnayla komut, CancellationToken ct)
{
var siparis = await repo.GetirAsync(komut.SiparisId, ct)
?? throw new NotFoundException("Sipariş bulunamadı.");
siparis.Onayla(); // kural domain'de
await uow.KaydetAsync(ct); // kayıt sonrası domain event'ler yayımlanır
}
}
public class StokRezerveEt(IStokServisi stok) : INotificationHandler<SiparisOnaylandi>
{
public Task Handle(SiparisOnaylandi olay, CancellationToken ct) => stok.RezerveEtAsync(olay.SiparisId, ct);
}
Burada mediator, DDD'nin haberleşme altyapısını sağlıyor; DDD ise mediator'ün taşıdığı mesajlara anlam veriyor.
5. Hangisine ne zaman ihtiyacınız var?
- Projeniz küçük ve kuralları basitse: İkisine de ihtiyacınız olmayabilir. Düz servisler ve iyi isimlendirme yeter.
- Use-case sayısı arttı, controller'lar şişti, her yerde aynı loglama-doğrulama kodu tekrar ediyor: Mediator iyi bir adım.
- İş kuralları karmaşık, iş birimiyle sürekli "biz bunu öyle demiyoruz" tartışması yaşanıyor, bir değişiklik beklenmedik yerleri bozuyor: DDD düşünme zamanı.
- Büyük, uzun ömürlü bir kurumsal sistem: Çoğu zaman ikisi birlikte.
Son söz
Mediator kodunuzu düzenler, DDD ise kodunuza anlam kazandırır. Birini diğerinin yerine koymak, bir binanın elektrik tesisatını iyileştirip mimari planını çözdüğünü sanmaya benzer. Önce probleminizi teşhis edin: Bağımlılıktan mı muzdaripsiniz, yoksa işi yanlış mı modelliyorsunuz? Cevap, hangi araca uzanmanız gerektiğini zaten söyleyecektir.
Kaynaklar: Erich Gamma vd., Design Patterns (1994) · Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003) · Vaughn Vernon, Implementing Domain-Driven Design (2013)
Yorumlar (0)