Value Objects kavramı, immutability ve equality semantics. Entity'lerden farkları ve domain modeling'de kullanım senaryoları.
DDD
Value Objects
Domain
Immutable
Equality
🧱 Value Object Nedir?
Value Object, kimliği olmayan ve tamamen değeriyle tanımlanan nesnelerdir. Yani bir Value Object’in kimliği yoktur; içeriği aynıysa, o nesne eşittir. Genellikle Immutable yani değiştirilemez olması amaçlanır.
📌 Temel Özellikleri:
Özellik
Açıklama
🆔 Kimlik yoktur
Herhangi bir Id alanı içermez. Kimliksizdir.
📦 Değere göre eşitlik
İçerdiği tüm alanlar aynıysa, iki VO eşittir.
♻️ Immutable’dır
Değeri değiştirilemez; yeni bir VO oluşturulur.
🎯 Anlamlı bir kavramı temsil eder
Örneğin: Adres, Para, Koordinat, Tam Ad, Email gibi.
Value object kavramı aslında ucu bucağı olmayacak bir kavram.
Şimdi beraber bir örnek üzerinden ilerleyelim.
bash
public sealed class User: Entity
{ public User(Guid CreatedTimeAssignedId): base(CreatedTimeAssignedId){} public string Name { get;set;} public string Email { get;set;} public string Password { get;set;} public string Country { get;set;} public string City { get;set;} public string Street { get;set;} public string FullAddress { get;set;} public string PostalCode { get;set;}}
Gördüğünüz gibi burada bizim bir adet entity desenimiz var ilk gördüğünüzde herhangi bir problem yokmuş gibi gözüküyor değil mi ?
Ama aslında bu DDD'ye göre biraz problemli bir yapı , problemli diyorum çünkü bir kaç tane dezavantajı var. Nedir bunlar derseniz ilk söyleyeceğim ben bu class'dan bir obje oluşturduğum zaman Name değerini abc yaparken Email kısmınıda abc olarak ayarlayabilirim değil mi ?
Yani bu yapı klasik bir EF Core veya düz OOP modellemesidir. Her alan string olarak tutulur. Ancak bu yaklaşımda:
Anlamsal bir yapı yoktur.
Her string aynı değeri taşıyabilir: "123" hem postal kod, hem ad, hem şifre olabilir.`
Address` gibi bir kavram, sadece bir grup string olarak görülür. Kurallar, validasyonlar dağınıktır.
Reusability zayıftır.
Şimdi beraber adım adım beraber Value Objecler oluşturarak ilerleyelim.
Öncelikle Reusability olarak oluşturulacak bir Obje yapısı düşünelim, DRY yapısına uygun olsun.
Benim aklıma öncelikle Address yapısından başlamak geldi.
bash
public sealed record AddressRecord( string Country ,
string City ,
string Street ,
string FullAddress ,
string PostalCode);
Şimdi gördüğünüz gibi bir Address Record Nesnesi ürettik Neden Record Ürettik kısa bir şekilde açıklamak gerekirse.
Çünkü:
Record tipi otomatik olarak value-based equality (yani değer temelli eşitlik) sağlar.
Equals(), GetHashCode(), ToString() gibi metotları otomatik olarak override eder.
Immutable olması kolaydır (default olarak init-only özellikleri vardır).
Ama burada şu konuyu atlamış oluyoruz yukarıda ne demiştik Kurallar, validasyonlar dağınıktır.
Hmm kural eklememiz gerekecek o zaman değil mi ?
Hemen ekleyelim.
bash
public sealed record AddressRecord( string Country,
string City,
string Street,
string FullAddress,
string PostalCode){ public AddressRecord
{if(string.IsNullOrWhiteSpace(Country)) throw new ArgumentException("Country boş olamaz.");if(string.IsNullOrWhiteSpace(City)) throw new ArgumentException("City boş olamaz."); // Diğer kurallar...
}}
Bu kadar basit Artık oluşturduğumuz bu record sayesinde hem kurallı olan bir yapımız var.
Ayrıca bunu ayrı bir dosya olarak örnek olarak Shared klasörü altında toplarsak Reusability yaparak DRY yapısından da kurtulmuş oluruz.
Ama ben class olarak kullanmak istiyorum derseniz burada atlamamamız gereken önemli bir nokta var.
bash
public sealed class Address
{ public string Country { get;} public string City { get;} public string Street { get;} public string FullAddress { get;} public string PostalCode { get;} public Address(string country, string city, string street, string fullAddress, string postalCode){ Country = country; City = city; Street = street; FullAddress = fullAddress; PostalCode = postalCode;} public override bool Equals(object obj){if(obj is not Address other)returnfalse;return Street == other.Street && City == other.City && Country == other.Country;} public override int GetHashCode(){return HashCode.Combine(Street, City, Country);}}
❗️Dikkat
Şimdi dikkat ederseniz property tanımlarımızda sadece {get;} kullandık.
Sebebi daha önce dediğim gibi Value Object gibi yapılarda, bir kere oluşturulduktan sonra değerin sabit kalması istenir.
Böylece:
Daha güvenli olur (hatalı dış müdahale engellenir), Test edilebilirliği ve öngörülebilirliği artar.
❓️ public string Value { get; } yerine get; private set; mi, get; init; mi, başka bir erişim belirleyici mi kullanmalıyım?
Bu sorunun cevabı şuna bağlı:
Value Object’in bu property'si nereden ve ne zaman değiştirilecek?
🔐 Cevap: DDD’de (özellikle Value Object’lerde) doğru tercih genellikle:
public string Value { get; }
public string Value { get; init; }
Ama hangisini ne zaman seçeceğini şöyle özetleyeyim:
✅ 1. public string Value { get; }
Sadece okunabilir, dışarıdan değiştirilemez.
Sadece constructor içinde atanabilir.
Value Object'lerde en katı ve güvenli olanıdır.
Immutable (değiştirilemez) tasarım için idealdir.
📌 Önerilen: ✔️
✅ 2. public string Value { get; init; }
C# 9 ve sonrası destekler.
Sadece ilk oluştururken (object initializer veya constructor) atanabilir.
Sonradan set edilemez.
public Email Email { get; init; } // sadece ilk atamada kullanılabilir
📌 Immutable gibi davranır ama record tipleriyle daha sık kullanılır.
⚠️ 3. public string Value { get; private set; }
Dışarıdan okunabilir, sadece sınıf içinden değiştirilebilir.
Value Object için riskli: değiştirilebilirlik açar.
public string Value { get; private set; } // class içinden set edilebilir ❌
📌 Value Object için önerilmez. Mutable olur.
⚠️ 4. protected set , internal set gibi diğer erişimler
protected set: Alt sınıflar değiştirebilir.
internal set: Aynı assembly içinden değiştirilebilir.
Bunlar Entity’lerde anlamlı olabilir ama Value Object için yine değiştirilebilirlik açığı doğurur.
Şimdi biz Address tarafını düzenledik birde E-Mail için düzenleme yapalım daha iyi pekiştirme yapabilmek için.
bash
public sealed record Email
{ public string Value { get;} public Email(string value){if(string.IsNullOrWhiteSpace(value)){ throw new ArgumentException("Email cannot be null or empty", nameof(value));} Value = value; if(string.Compare(value, "example.com", StringComparison.OrdinalIgnoreCase)==0){ throw new ArgumentException("Email cannot be example.com", nameof(value));} if(!value.Contains("@")){ throw new ArgumentException("Email must contain '@'", nameof(value));}}}
Hem Adres için hemde Email için gördüğünüz gibi küçük Value Object sınıfları oluşturduk.
string Email ne demek? Herhangi bir metin olabilir. Ama Email adında bir Value Object sınıfı varsa, artık o sadece bir metin değil, geçerli bir e-posta adresini temsil eden anlamlı bir kavram olur.
✅ Artık Email tipindeki bir değişken sadece "abc" olamaz. Çünkü oluşturulurken validasyon yapılır. Bu da domain kurallarını korumanı sağlar.
bash
public sealed class User : Entity
{ public User(Guid CreatedTimeAssignedId): base(CreatedTimeAssignedId){} public Name Name { get;set;} public Password Password { get;set;} public Email Email { get;set;} public AddressRecord Address { get;set;} // Address sınıfını burada kullanıyoruz
}
Şimdi eklediğimiz Value Object nesneleri Tip olarak tanımadık işimiz bitti mi hayır.
Problem Nedir ?
bash
User kubilay = new(Guid.NewGuid()); kubilay.Email = new(""); //Erişip değişiklik yapabiliyorum.
Console.Write(kubilay.Name);
Yukarıda gördüğünüz gibi ben bir User objesi oluşturduktan sonra bu objenin Email değerini değiştirebiliyorum.
Bu istemediğimiz bir durum DDD 'de uygulamaya çalıştığımız kuralı burada çiğnemiş oluyoruz.
Ana kuralımız neydi ?
DDD’de domain modelleri (Entity ve Value Object’ler) kendi tutarlılığından (invariant) sorumludur. Yani, bu nesnelerin her zaman geçerli ve tutarlı durumda olması gerekir.
Eğer public set kullanırsak, dışarıdan istediğimiz gibi doğrudan ve kontrolsüz şekilde property değerleri değiştirilebiliriz.
Bunu engellememiz için ve tutarlılığı sağlayabilmemiz için bizim ilk önce yapmamız gereken şey User nesnesi oluşurken bu değerleri atamak eğer User nesnesinde daha sonra bu nesneleri yada değerleri değiştirmek istiyorsak bunu User Entity'si içinde yapmamız gerekir.
İşte bunu sağlamak için public set yerine private set kullanmamız gerekir.
bash
public sealed class User : Entity
{ public User(Guid CreatedTimeAssignedId): base(CreatedTimeAssignedId){} public Name Name { get; private set;} public Password Password { get; private set;} public Email Email { get; private set;} public AddressRecord Address { get; private set;} // Address sınıfını burada kullanıyoruz
}
Peki şimdi kontrol edelim.
bash
User kubilay = new(Guid.NewGuid()); kubilay.Email = new(""); //Erişip değişiklik yapabiliyorum.
Console.Write(kubilay.Name);
The property or indexer 'User.Email' cannot be used in this context because the set accessor is inaccessible hatası alırız Yani kardeşim hoop böyle bir değişim yapamazsın diyor compiler bize.
Peki değişim yapmamız gerekti nasıl yapıcaz ?
Daha önceden dediğim gibi Entity içinde değişim yapacağız.
bash
public sealed class User : Entity
{ public User(Guid CreatedTimeAssignedId): base(CreatedTimeAssignedId){} public Name Name { get; private set;} public Password Password { get; private set;} public Email Email { get; private set;} public AddressRecord Address { get; private set;} // Address sınıfını burada kullanıyoruz
public void UpdateName(Name name){ Name = name;}}
Şimdi kontrol edelim.
bash
User kubilay = new(Guid.NewGuid());kubilay.UpdateName(new("dddddddd")); //Doğrudan Erişip değişiklik yapamıyorum.
Console.Write(kubilay.Name);
Atladığımız bir nokta var.
Propertylerimiz bizim private set olarak ayarlandı dışarıdan ulaşamıyoruz. User oluşurken nasıl Name,Password gibi değerleri assign edeceğiz?
Constructor ile tabiki.
bash
public sealed class User : Entity
{ public User(Guid Id,Name name, Password password, Email email, AddressRecord address):base(Id){ Name = name; Password = password; Email = email; Address = address;} public Name Name { get; private set;} public Password Password { get; private set;} public Email Email { get; private set;} public AddressRecord Address { get; private set;} // Address sınıfını burada kullanıyoruz
public void UpdateName(Name name){ Name = name;}}
Şimdi son kez tekrar edelim.
bash
User kubilay = new(Guid.NewGuid(),new("Kubilay"),new("1233131"),new("kubilay@bozak.dev"),new("Türkiye","İstanbul","Test","Test Full Address","3124")); Console.Write(kubilay.Name); //Kubilay değerini döner
kubilay.UpdateName(new("dddddddd")); //Doğrudan Erişip değişiklik yapamıyorum.
Console.Write(kubilay.Name); //dddddddd değerini döner
Ufak bir pekiştirme.
bash
//Id değerleri farklı
User user1 = new(Guid.NewGuid(), new("Kubilay"),new("1233131"),new("kubilay@bozak.dev"),new("Türkiye","İstanbul","Test","Test Full Address","3124")); User user2 = new(Guid.NewGuid(), new("Kubilay"), new("22222222222"), new("kubilay2@bozak2.dev"), new("Türkiye2", "İstanbul", "Test", "Test Full Address", "3124")); //Id değerleri aynı
User user3 = new(oneTimeCreatedGuid, new("Kubilays"), new("1233131"), new("kubilay@bozak.dev"), new("Türkiye", "İstanbul", "Test", "Test Full Address", "3124")); User user4 = new(oneTimeCreatedGuid, new("Kubilay"), new("22222222222"), new("kubilay2@bozak2.dev"), new("Türkiye", "İstanbul", "Test", "Test Full Address", "3124")); Console.WriteLine(user1.Name.Equals(user2.Name) +" Name değerleri user1 ve user 2"); //True döner Value Object'ler ve içerikleri aynı
Console.WriteLine(user1.Address.Equals(user2.Address) +" Address değerleri user1 ve user 2"); //False döner içerikleri farklı
Console.WriteLine(user1.Equals(user2) + " Userdeğerleri user1 ve user2"); //False döner çünkü User sınıfı Entity'dir ve Id'leri farklıdır.
Console.WriteLine(user3.Name.Equals(user4.Name) + " Name değerleri user1 ve user 2"); //True döner Value Object'ler ve içerikleri aynı
Console.WriteLine(user3.Address.Equals(user4.Address) + " Address değerleri user1 ve user 2"); //True döner içerikleri aynı
Console.WriteLine(user3.Equals(user4) + " Userdeğerleri user1 ve user2"); //True döner çünkü User sınıfı Entity'dir ve Id'leri aynıdır.
❗️Zararları ya da Zorlukları?
Zorluk/Fark
Açıklama
👨💻 Kod biraz daha karmaşık görünür
Her alan için ayrı bir sınıf yazmak gerekir
🧠 Öğrenme eğrisi
EF Core ve klasik yaklaşım alışkanlığı olanlar için alışmak zaman alır
🧱 EF Core mapping ayarları gerekir
Value Object’leri map’lemek için Owned Entity kullanman gerekir (ama kolay bir ayardır)
✅ Ama Sonuç?
Modelin daha anlamlı, sağlam, tekrar kullanılabilir ve domain kurallarını koruyucu bir hale gelir.
Bu, DDD’nin "zengin domain modeli" (Rich Domain Model) yaklaşımının temelidir.
İlk yapıda her şey string’di, domain kuralları yoktu.
İkinci yapıya geçince: