Domain-Driven Design: Aggregate, Value Object ve Bounded Context ile İş Alanını Modellemek
Domain-Driven Design (DDD), Eric Evans'ın aynı adlı kitabıyla hayat bulan, karmaşık iş alanlarını (business domains) yazılım modellerine dönüştürme disiplinidir. DDD, "Veritabanı nasıl görünmeli?" değil, "Bu iş alanı nasıl çalışır ve kuralları nelerdir?" sorusuna odaklanır. İki ana katmandan oluşur: Stratejik Tasarım (Strategic) ve Taktik Tasarım (Tactical). Bu yazıda, her iki katmanın temel yapı taşlarını kod örnekleriyle inceleyeceğiz.
1. Stratejik Tasarım: Bounded Context ve Ubiquitous Language (Ortak Dil)
DDD'nin en büyük katkısı, domain uzmanları (iş analistleri, ürün sahipleri) ile geliştiriciler arasında ortak bir dil (Ubiquitous Language) oluşturulmasıdır.
-
Ubiquitous Language (Ortak Dil): Kodunuzdaki sınıf isimleri, metot isimleri, değişkenler, iş toplantılarında kullanılan terimlerle birebir aynı olmalıdır. Örneğin, iş dünyasında "Sipariş iptal etme" deniyorsa, kodda
CancelOrder()metodu olmalı,DeleteOrder()değil. -
Bounded Context (Sınırlı Bağlam): Büyük bir sistem, farklı anlamlar içerebilir. "Müşteri" kavramı, Satış bağlamında (CRM) farklı, Destek bağlamında (Ticket sistemi) farklı özelliklere sahip olabilir. Her bağlam (context) kendi modeline sahiptir ve diğer bağlamlarla arasına net bir sınır (boundary) çizer. Bağlamlar arası iletişim, Context Mapping (Paylaşılan Kernel, Anti-Corruption Layer vb.) ile sağlanır.
2. Taktik Tasarım: Temel Yapı Taşları
Stratejik tasarım sınırları çizdikten sonra, bu sınır içinde modelleri inşa etmek için taktik kalıplar kullanılır.
A. Entity (Varlık)
-
Tanım: Zaman içinde sürekliliği olan, kimliği (Identity) ile tanımlanan nesnelerdir. Özellikleri (attributes) değişebilir ama kimliği asla değişmez.
-
Örnek: Bir
Order(Sipariş) nesnesi. Müşteri adı, adresi değişebilir ama o siparişinOrderId'si her zaman aynıdır. -
Kod:
Idproperty'si bulunur. Eşitlik (Equals)Idüzerinden yapılır.
B. Value Object (Değer Nesnesi)
-
Tanım: Kimliği (Identity) olmayan, sadece sahip olduğu niteliklerin (attributes) bütünü ile tanımlanan, değişmez (immutable) nesnelerdir. İki Value Object, tüm nitelikleri eşitse birbirine eşittir.
-
Örnek:
Address(Şehir, Sokak, Posta Kodu),Money(Para Birimi, Tutar),DateRange(Başlangıç-Bitiş). -
Kod:
recordveyareadonly structile implemente edilir. Setter'ı yoktur; değiştirmek için yeni bir örnek oluşturulur.
csharp
// Value Object (C# 9 Record ile mükemmel uyum)
public sealed record Money
{
public string Currency { get; }
public decimal Amount { get; }
public Money(string currency, decimal amount)
{
if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Para birimi boş olamaz.");
if (amount < 0) throw new ArgumentException("Tutar negatif olamaz.");
Currency = currency.ToUpperInvariant();
Amount = amount;
}
public Money Add(Money other)
{
if (Currency != other.Currency) throw new InvalidOperationException("Farklı para birimleri toplanamaz.");
return new Money(Currency, Amount + other.Amount);
}
}
C. Aggregate (Küme) ve Aggregate Root (Kök Varlık)
-
Tanım: Aggregate, birlikte ele alınması gereken Entity ve Value Object'lerden oluşan bir kümedir. Bu kümenin dış dünyaya açılan tek kapısı, Aggregate Root (Kök Varlık)'tur. Dış dünya, Aggregate Root üzerinden işlem yapar; altındaki Entity'lere doğrudan erişemez.
-
Görevi: Aggregate, kendi içindeki tutarlılık kurallarını (invariants) korumakla yükümlüdür. Örneğin, bir siparişin toplam tutarı, sipariş kalemlerinin toplamına eşit olmalıdır. Bu kuralı sadece Aggregate Root sağlayabilir.
-
Transaction Sınırı: Bir Aggregate'in tamamı, bir veritabanı transaction'ında güncellenmesi gereken en küçük birimdir. İki farklı Aggregate asla aynı transaction'da güncellenmez (Eventual Consistency kullanılır).
csharp
// Entity (OrderItem - Aggregate içindeki alt entity)
public class OrderItem
{
public Guid Id { get; private set; }
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public Money UnitPrice { get; private set; }
internal OrderItem(string productName, int quantity, Money unitPrice)
{
Id = Guid.NewGuid();
ProductName = productName;
Quantity = quantity;
UnitPrice = unitPrice;
}
public Money GetTotal() => UnitPrice.Add(new Money(UnitPrice.Currency, UnitPrice.Amount * Quantity));
}
// Aggregate Root (Order)
public class Order
{
// Kimlik (Identity)
public Guid Id { get; private set; }
public string CustomerName { get; private set; }
public Money TotalAmount { get; private set; }
public DateTime CreatedAt { get; private set; }
// Aggregate içindeki alt entity'ler (Sadece Order üzerinden yönetilir)
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
private Order() { } // ORM için
public Order(string customerName)
{
Id = Guid.NewGuid();
CustomerName = customerName ?? throw new ArgumentNullException(nameof(customerName));
CreatedAt = DateTime.UtcNow;
TotalAmount = new Money("TRY", 0);
}
// Aggregate Root üzerinden işlem (Davranış)
public void AddItem(string productName, int quantity, Money unitPrice)
{
if (quantity <= 0) throw new ArgumentException("Adet pozitif olmalıdır.");
// İş kuralı: Aynı ürün varsa, miktarını güncelle
var existingItem = _items.FirstOrDefault(i => i.ProductName == productName);
if (existingItem is not null)
{
// Domain servis veya özel metot ile miktar güncellemesi...
return;
}
var newItem = new OrderItem(productName, quantity, unitPrice);
_items.Add(newItem);
// Toplam tutarı yeniden hesapla (Tutarlılık - Invariant)
RecalculateTotal();
}
private void RecalculateTotal()
{
if (!_items.Any())
{
TotalAmount = new Money("TRY", 0);
return;
}
var firstCurrency = _items.First().UnitPrice.Currency;
var total = _items.Sum(i => i.GetTotal().Amount);
TotalAmount = new Money(firstCurrency, total);
}
}
D. Domain Event (Alan Olayı)
Aggregate'ler arası iletişim için kullanılır. Bir Aggregate Root'da önemli bir değişiklik olduğunda (ör. OrderPlaced), bu olay fırlatılır ve diğer Aggregate'ler (ör. Inventory) bu olayı dinleyerek stok düşer. Bu, gevşek bağlantı ve eventual consistency sağlar.
3. Anemic Domain (Kansız Model) Anti-Pattern'ına Dikkat!
Kansız Model: Sadece veri taşıyan (getter/setter property'leri olan), hiçbir iş mantığı (davranış) içermeyen sınıflardır. Bu, DDD'ye aykırıdır çünkü domain kuralları servis katmanına (Service Layer) taşınır ve kod dağılır.
✅ Doğru (Zengin Model - Rich Domain):
csharp
// Order sınıfında AddItem metodu var, order total'i kendi hesaplıyor. // Servis katmanı sadece order.AddItem(...) çağırır.
❌ Yanlış (Anemic):
csharp
// Order sadece property'lerden ibaret.
public class Order { public List<OrderItem> Items { get; set; } }
// Servis katmanı:
var order = new Order();
order.Items.Add(newItem);
decimal total = order.Items.Sum(i => i.Price * i.Quantity); // İş mantığı burada!
4. Repository (Depo) ve Factory (Fabrika) Rolü
-
Repository: Aggregate Root'leri saklamak (veritabanı) ve geri getirmek için kullanılır. Sadece Aggregate Root için repository yazılır; alt Entity'ler için ayrı repository yoktur.
-
Factory: Karmaşık Aggregate oluşturma mantığını kapsüllemek için kullanılır.
csharp
public interface IOrderRepository
{
Task<Order> GetByIdAsync(Guid id);
Task AddAsync(Order order);
Task UpdateAsync(Order order);
}
⚠️ Kritik Uyarı: Repository, veritabanı sorgu detaylarını (örn. IQueryable) domain katmanına sızdırmamalıdır. Çoğu kişi IOrderRepository içine GetByCustomerAsync gibi metotlar ekler. Daha temiz bir yaklaşım, Specification (Spesifikasyon) deseni veya doğrudan MediatR Query/Handler'ların repository çağırmasıdır.
5. Ne Zaman DDD Kullanmalı? (Basit CRUD'a Hayır!)
DDD, ağır bir mimaridir. Her projede kullanmak aşırı mühendisliktir (over-engineering).
| Uygun Olduğu Senaryolar | Uygun Olmadığı Senaryolar |
|---|---|
| Karmaşık, değişken iş kuralları (ör. sigorta, bankacılık, borsa) | Basit veritabanı CRUD işlemleri (Admin paneli, blog). |
| Uzun ömürlü, evrilen projeler | Prototipler veya tek seferlik projeler. |
| Domain uzmanlarının (iş analistleri) sürece dahil olduğu projeler | Teknik ekibin iş alanına uzak olduğu projeler. |
| Event Sourcing ve CQRS ile birlikte kullanılacaksa |
Sonuç:
DDD, sadece bir tasarım deseni değil, bir zihniyet değişikliğidir. Önce iş alanını (domain) anlamak, doğru sınırları (Bounded Context) çizmek, ortak bir dil (Ubiquitous Language) oluşturmak ve bu dili kodun içine (Entity, Value Object, Aggregate) yerleştirmek gerekir. Entity'leri kimlikle, Value Object'leri değerleriyle, Aggregate'leri ise tutarlılık sınırlarıyla düşünün. Tüm bu yapıyı, servis katmanına iş mantığı sızdırmadan (Kansız Model'den kaçınarak) inşa edin. Kodunuz artık sadece veri taşımaz; iş kurallarını taşır ve uygular.