Value Objects Nedir? Domain-Driven Design(DDD) Yaklaşımındaki Rolü Nedir?
Merhaba,
Value Objects, programlama süreçlerinde entity’ler gibi kendisine ait kimliğe yani id değerine sahip olmayan lakin taşıdıkları değerlerle anlam ifade eden nesnelerdir ve DDD yaklaşımında önemli rol oynayan konseptlerden birisidir. Haliyle bu açıdan baktığımızda nesnenin kimliği onun sahip olduğu özellikler tarafından kümülatif olarak tanımlanmaktadır diyebiliriz. Evet, bu da akıllara record‘ları getirmektedir. Biliyorsunuz ki record’lar, bir nesnedir lakin değerleri ön planda olan bir nesnedir. Yani aynı değerlere sahip olan iki farklı record nesnesi karşılaştırıldığında bu bizim için aynı olarak değerlendirilmektedir. İşte value object’lerde de mantık tam da budur. Yani aynı değerlere sahip olan iki value object, fiziksel olarak farklı nesneler olsalar da teoride aynı kabul edilmektedirler. Çünkü bu nesnelerin kimlikleri, taşıdıkları özellikler ve değerler üzerinden bütünsel elde edilmektedir. Bu yüzden value object’ler değiştirilemezdirler(immutable) Dolayısıyla bir value object üzerinde değişiklik yapılması gerektiğinde genellikle yeni bir nesne oluşturulmakta ve değişiklik bu nesneye yansıtılmaktadır.
Value Object’ler Nasıl Tanımlanır?
C#’ta value object tasarımı için aşağıdaki gibi dikkat edilmesi gereken birkaç husus mevcuttur.
- Value object’ler, yukarıdaki satırlarda bahsedildiği gibi değiştirilemez olmalıdırlar. Bu yüzden tüm field ve property’leri readonly olarak tanımlanmalıdır.
- Value object’lerin değerleri ön planda olacağından dolayı eşitlik durumlarında bu mantıkta bir karşılaştırma yapılmalıdır. Bu yüzden Equals ve GetHashCode metotları property’lere dayalı eşitliği sağlamak için override edilmelidir.
. . . public override int GetHashCode() => (AProperty, BProperty).GetHashCode(); public override bool Equals(object? obj) { if (obj is X other) return AProperty == other.AProperty && BProperty == other.BProperty; return false; } . . . - Ve son olarak value object’ler taşıyacakları değerlere dair geçerlilik kontrollerini kendileri üstlenmelidirler. Bu durum self-validation / öz doğrulama olarak nitelendirilmektedir. Bu davranış sembolik olarak aşağıdaki gibi olabilir;
class X { public int AProperty { get; set; } public int BProperty { get; set; } public X(int aProperty, int bProperty) { if (aProperty < 0) throw new ArgumentException($"{nameof(X.AProperty)} cannot be negative."); if (bProperty < 0) throw new ArgumentException($"{nameof(X.BProperty)} cannot be negative."); AProperty = aProperty; BProperty = bProperty; } . . . }
Tüm bu hususları gözeterek aşağıdaki gibi gerçekçi bir value object tanımlayabiliriz;
class Money
{
public decimal Amount { get; init; }
public string Currency { get; init; }
public Money(decimal amount, string currency)
{
//Self-validation
Validate(amount, currency);
Amount = amount;
Currency = currency;
}
public override bool Equals(object? obj)
{
if (obj is Money other)
return Amount == other.Amount && Currency == other.Currency;
return false;
}
public override int GetHashCode()
=> (Amount, Currency).GetHashCode();
private void Validate(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException($"Amount cannot be negative.");
if (string.IsNullOrEmpty(currency))
throw new ArgumentException($"Currency cannot be null or empty.");
}
}
Yukarıdaki örnek kod bloğunu incelerseniz eğer ilgili sınıfın constructor’ından gerekli değerler parametre olarak alınmakta ve Validate metodu aracılığıyla iş kuralı gereği self-validation gerçekleştirilmektedir. Bu kontrolün constructor’da yapılmasıyla, ilgili value object’in kuralları dışında bir instance’ın ayağa kaldırılması engellenmektedir. O yüzden value object tasarımlarında self-validation davranışı genellikle yapıcıda gerçekleştirilmektedir.
Evet, görüldüğü üzere value object aşağı yukarı bu mantıkta tanımlanmakta ve bir nesneden ziyade bütünsel olarak bir veriye karşılık gelen olguyu ifade etmektedir. Tabi bu tanım, tecrübe edildiği üzere içerisinde diğer nesnelerle olacak olan karşılaştırmaların fonksiyonel tanımlarını da(Equals, GetHashCode vs.) barındırmakta ve bu esasında operatörler için de uygulanması gereken daha kompleks bir çalışma gerektirmektedir. Nihayetinde bu nesneler madem veriye karşılık gelmektedirler o halde kendi aralarında == yahut != operatörleriyle de karşılaştırılabilmelidirler. Doğru değil mi?
İşte bu durumda bu operatörleri tanımladığımız tüm value object’ler için overload etmeli ve davranışı veri öncelikli olacak şekilde kurgulamalıyız. Ee bu ihtiyacı tüm value object’ler için ameleus mantığında tekrar tekrar yapmaktansa aşağıdaki gibi tasarlanmış bir base class’ta karşılamak hem kodlama açısından daha makul olacak, hem de biryandan uygulamadaki value object’lerin zahiren marker’ını da oluşturmuş olacağız.
abstract class ValueObject : IEquatable<ValueObject>
{
protected abstract IEnumerable<object> GetEqualityComponents();
private bool ValuesAreEqual(ValueObject valueObject)
=> GetEqualityComponents().SequenceEqual(valueObject.GetEqualityComponents());
public virtual bool Equals(ValueObject? other)
=> other is not null && ValuesAreEqual(other);
public override bool Equals(object? obj)
=> obj is ValueObject valueObject && ValuesAreEqual(valueObject);
public override int GetHashCode()
=> GetEqualityComponents().Aggregate(default(int), (hashCode, value) => HashCode.Combine(hashCode, value.GetHashCode()));
public static bool operator ==(ValueObject? left, ValueObject? right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
return false;
return left.Equals(right);
}
public static bool operator !=(ValueObject? left, ValueObject? right)
=> !(left == right);
}
Yukarıdaki kod bloğundaki metotları tek tek incelersek eğer;
GetEqualityComponentsmetodu ile bu abstract class’ı implemente edecek olan value object’lerdeki hangi property’lerin değersel karşılaştırmaya tabi tutulacağı referans edilmektedir. Haliyle bu metodun içeriği concrete class’lar da belirlenecektir.ValuesAreEqualmetodunda ise ilgili value object’inGetEqualityComponentsmetodunda belirtilen property’lerinin değersel eşitlik durumu değerlendirilmektedir. Bu değerlendirme sürecinde dikkat ederseniz koleksiyonel değer karşılaştırmamızı yapmamızı sağlayanSequenceEqualmetodu kullanılmaktadır.==ve!=operator overloading’ler ise ilgili operatörlere gerekli davranışları kazandırmaktadır.
Evet, böylece artık bir value object’i tanımlamamızı sağlayacak olan base class’ı tam teferruatlıca oluşturmuş bulunuyoruz. Artık bu çalışma neticesinde uygulamadaki value object’leri aşağıdaki gibi ‘ValueObject’ implementasyonu neticesinde aşikârene bir şekilde tanımlayabiliriz;
class Money : ValueObject
{
public decimal Amount { get; init; }
public string Currency { get; init; }
public Money(decimal amount, string currency)
{
//Self-validation
Validate(amount, currency);
Amount = amount;
Currency = currency;
}
private void Validate(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException($"Amount cannot be negative.");
if (string.IsNullOrEmpty(currency))
throw new ArgumentException($"Currency cannot be null or empty.");
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}
Evet, value object tanımlama sürecinde bu şekilde daha kullanışlı ve az maliyetli bir çalışma yapıldığı kanaatinde mutabık olduğumuzu düşünüyorum 🙂
record Kullanarak Value Object Tasarlama
record‘lar, yapısal olarak değerleri ön planda olan nesneleri temsil ettikleri için doğal olarak value object ihtiyaçlarını karşılayan nesnelerdir. Haliyle aşağıdaki gibi tanımlanmış bir record, üst satırlarda class ile tasarlamaya çalıştığımız value object’in esasında birebir aynı mahiyetinde varsayılan bir halidir ve direkt olarak value object’ten beklenen davranışları ve özellikleri sergilemektedir.
record Money(decimal Amount, string Currency);
record yapısal olarak immutable’dır. Tüm property’leri üzerinde otomatik olarak Equals ve GetHashCode metotlarını override etmektedir ve deconstruct özelliği sayesinde de kolayca parçalara ayrılabilmektedir.
Haliyle record’lar sayesinde class versiyonuna nazaran oldukça kısa ve pratik bir şekilde value object tanımı gerçekleştirilebilmektedir. Ancak record’ın getirdiği bu kolaylığa nazaran, ‘ValueObject’ base class’ını kullanarak yapılan çalışmalarda da yapılandırma ve özelleştirme açısından türlü avantajların olacağını da belirtmeden geçemeyeceğim.
Ayrıca record’lar da self-validation uygulanmak istendiğinde de yine benzer mantıkla aşağıdaki gibi sanki sınıflarda çalışıyormuşcasına gerçekleştirilebilmektedir;
record Money
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public Money(decimal amount, string currency)
{
//Self-validation
Validate(amount, currency);
Amount = amount;
Currency = currency;
}
private void Validate(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException($"Amount cannot be negative.");
if (string.IsNullOrEmpty(currency))
throw new ArgumentException($"Currency cannot be null or empty.");
}
}
Tabi bundan sonrası için programatik açıdan record’larla ilgili bilginiz ve tecrübeniz geçerli olacaktır.
EF Core – Persisting Value Objects
Value object’in kullanıldığı çalışmalarda, özellikle EF Core ile birlikte bu nesnelerin yönetilmesi ve değerlerinin veritabanına yansıtılması(persisting) oldukça önem arz eden kritik bir konudur. Çünkü bu nesnelerde primary key’e karşılık değer bulunmamakta ve genellikle bir entity eşliğinde veritabanında depolanmaktadırlar. Yani anlayacağınız hem id’leri yoktur, hem de ilişkisel mantıkta veritabanında tutulmak mecburiyetindedirler. Haliyle bir kimliği olmayan veriyi, kimliği olan bir veriyle ilişkilendirip tutmak her ne kadar kulaklara mantıksız geliyor olsa da esasında EF Core açısından bu durum Owned Types ve Complex Types kavramları eşliğinde çözüme kavuşturulmaktadır. Şimdi gelin bu kavramlar eşliğinde EF Core’da value object kullanımını değerlendirmeye başlayalım.
- Owned Types
Tee zamanında Entity Framework Core – Owned Entities and Table Splitting başlıklı makalemizle incelediğimiz bu özellik sayesinde bir entity’i parçalayabilmekte ve fiziksel olarak farklı class’lardan meydana getirebilmekteyiz.
Görüldüğü üzere bu mantıkla bir entity’nin kod tarafında value object olarak ayrılan kısmı veritabanına esasında bir bütün olarak yansıtılabilmektedir.
- Complex Types
.NET 8 ile gelmiş olan yeni bir EF Core özelliğidir. Owned Types’a benzer işleve sahip olsa da, davranışsal olarak daha esnek ve gelişmiş bir yapıdadır. Complex Type’lar, birden fazla entity tarafından kullanılabilmekte böylece tekrar kullanılabilirlik daha elverişli olabilmektedir. Ayrıca complex type’lar, ayrı bir tabloda saklanarak daha esnek bir veri modeli de oluşturmamıza imkan tanıyabilmektedir.
Peki, Value Object’ler Hangi Durumlarda Kullanılır?
Aslında bu sorunun cevabı oldukça basit, ihtiyaç olduğunda 🙂 Ama tabi kullanım sınırlarını biraz daha netleştirmemiz gerekirse eğer domain değişmezlerini kapsüllemek veya dışarıdan gelecek olan değer türlü verileri tür güvenli bir şekilde elde edebilmek için kullanılacağına dair bir genellemede bulunabiliriz. Misal olarak;
interface ILeaveService
{
int CalculateLeaveDays(User user, DateOnly startDate, DateOnly endDate);
}
Yukarıdaki interface içerisindeki CalculateLeaveDays imzası, görüldüğü üzere başlangıç ve bitiş tarihlerini parametre olarak almaktadır. Halbuki bu değer türlerini value object ile temsil ederek aşağıdaki gibi tür güvenli hale getirebiliriz;
interface ILeaveService
{
int CalculateLeaveDays(User user, DateRange dateRange);
}
record DateRange(DateOnly StartDate, DateOnly EndDate)
{
}
İşte özellikle kod arasında bu tarz durumlar için value object’ler oldukça efektiftir.
Onion Architecture’da Value Object’ler Hangi Katmanda, Nerede Tanımlanmalıdır?
Onion Architecture‘da domain ile ilgili temel iş kuralları ve yapı taşları Domain(Core) katmanında tanımlanmaktadır, ee haliyle value object’ler de bu katmanda tutulmalıdırlar. Çünkü her ne kadar entity’den farklı olsalar da özünde bir sistemin iş kuralını temsil etmektedirler ve bu minvalde diğer katmanlardan bağımsızlık sergilemeleri gerekmektedir.
Evet, böylece value object kavramını teorik ve pratik olarak incelemiş ve yazılım süreçlerindeki esas mahiyetine dair gerekli kritiği yapmış bulunuyoruz. Bundan sonrası artık sizlerin ihtiyaçları doğrultusunda ilgili yapıyı kullanmanız ve edineceğiniz tecrübelerinizle bizlere dönütler sağlayıp var olan eksikleri de tamamlatmanızdır 🙂
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
