ادامه از : Entities, Value Objects, Aggregates and Roots
قبلا و بصورت کامل در لینک بالا مفهوم Value Object ها را با هم مطالعه کردیم
شیئی که جنبه توصیفی از دامنه را بدون هویت مفهومی نشان می دهد، OBJECT VALUE نامیده می شود.
(اریک ایوانز)
دو موجودیت با ویژگی های یکسان اما با شناسه های متفاوت به عنوان موجودیت های متفاوت در نظر گرفته می شوند. با این حال، Value Object ها هیچ شناسه ای ندارند و در صورتی که دارای مقادیر یکسان باشند، برابر در نظر گرفته می شوند.
هویت برای موجودیت ها اساسی است. با این حال، بسیاری از اشیا و اقلام داده در یک سیستم وجود دارد که نیازی به ردیابی هویت ندارند، مانند Value Object ها.
همانطور که در شکل بالا نشان داده شده است، یک موجودیت معمولاً از چندین ویژگی(attributes) تشکیل شده است. برای مثال، موجودیت Order را میتوان بهعنوان موجودیتی با هویت مدلسازی کرد که از مجموعهای از ویژگیها مانند OrderId، OrderDate، OrderItems و غیره تشکیل شده است. street، city و غیره فاقد هویت(identity) در این حوزه(domain) است که باید به عنوان یک Value Object مدلسازی و با آن رفتار شود.
این بدان معناست که کلاس «Order» ما میتواند از:
public class Order { public int OrderId { get; private set; } public string Street { get; set; } public string City { get; set; } public string State { get; set; } public string Country { get; set; } public string ZipCode { get; set; } }
به شکل زیر تغییر یابد :
public class Order { public int OrderId { get; private set; } public Address Address { get; set; } }
با نگاهی به قطعه بالا، نسخه دوم کلاس Order خواناتر است و تأثیر مثبتی بر قابلیت نگهداری کد خواهد داشت.
همانطور که قبلا نیز توضیح داده شد دو ویژگی اصلی برای Value Objectها وجود دارد:
تغییر ناپذیری یک نیاز مهم است. پس از ایجاد شی، مقادیر یک Value Object باید تغییر ناپذیر باشند. بنابراین، هنگامی که شی ساخته می شود، باید مقادیر مورد نیاز را ارائه دهید، اما نباید اجازه دهید در طول عمر شیء تغییر کنند.
اما Value Objectها به دلیل ماهیت تغییرناپذیرشان به شما امکان می دهند ترفندهای خاصی را برای عملکرد پیاده سازی کنید. این امر به ویژه در سیستم هایی که ممکن است هزاران نمونه Value Object وجود داشته باشد که بسیاری از آنها مقادیر یکسانی دارند صادق است. ماهیت تغییرناپذیر شان به آنها امکان استفاده مجدد را می دهد. آنها می توانند اشیاء قابل تعویض باشند، زیرا مقادیر آنها یکسان است و هیچ هویتی ندارند. این نوع بهینه سازی گاهی اوقات می تواند بین نرم افزارهایی که کند اجرا می شوند و نرم افزارهایی با عملکرد خوب تفاوت ایجاد کند.
از نظر پیادهسازی، میتوانید یک کلاس پایه Value Object داشته باشید که این کلاس شامل عملیات اساسی مورد نیاز برای Value Objectهای ما است. در مثال زیر میتوانیم کدی را برای مقایسه برابر بودن یا نبودن دو Value Object ببینیم. (از آنجایی که یک Value Object نباید بر اساس هویت باشد)
در واقع مقایسه دو Object Value باید بر اساس مقادیر ویژگی این اشیا باشد. این بدان معنی است که دو Object Value با خصوصیات یکسان باید برابر باشند. به نمونه ای از پیاده سازی کلاسی مانند این نگاه کنید:
//Inspired from https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects //رویکرد مایکروسافت public abstract class ValueObject { protected static bool EqualOperator(ValueObject left, ValueObject right) { if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) { return false; } return ReferenceEquals(left, null) || left.Equals(right); } protected static bool NotEqualOperator(ValueObject left, ValueObject right) { return !(EqualOperator(left, right)); } protected abstract IEnumerable<object> GetEqualityComponents(); public override bool Equals(object obj) { if (obj == null || obj.GetType() != GetType()) { return false; } var other = (ValueObject)obj; return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); } public override int GetHashCode() { return GetEqualityComponents() .Select(x => x != null ? x.GetHashCode() : 0) .Aggregate((x, y) => x ^ y); } // Other utility methods }
وقتی از یک رویکرد مشابه Domain Driven Design استفاده می کنید، Value Object را نمی توان با یک شناسه شناسایی کرد، بلکه توسط فیلدهای موجود در شیء شناسایی می شود، بنابراین نیاز به عملگرهای برابری است که یک یا چند ویژگی را در شیء مقایسه می کند.
به همین دلیل است که نمیتوانید ویژگیها را تغییر دهید، زیرا شی با دیگر نمونههای بارگذاریشده آن قابل مقایسه نخواهد بود. مثل مثال بالا، اگر شماره خیابان را تغییر دهید، دیگر همان آدرس نیست.
هنگام پیاده سازی Value Object در DDD، باید تمام منطق مقایسه را در هر کلاس کپی کنید و بنابراین کدهای تکراری را انجام دهید. abstract class ValueObject نیاز به آن را برطرف میکند و اشیاء شما را خواناتر میکند.
کلاس فوق فقط چیزهای اساسی مانند مقایسه برابری و سایر اصول مورد نیاز برای EF را پیاده سازی می کند. علاوه بر این، کلاس های مشتق شده ما را نیز برچسب گذاری می کند.
شما می توانید از این کلاس هنگام پیاده سازی Value Object واقعی خود استفاده کنید، مانند Value Object Address که در مثال زیر نشان داده شده است:
public class Address : ValueObject { public String Street { get; private set; } public String City { get; private set; } public String State { get; private set; } public String Country { get; private set; } public String ZipCode { get; private set; } // Empty constructor in this case is required by EF Core, // because has a complex type as a parameter in the default constructor. private Address() { } public Address(string street, string city, string state, string country, string zipcode) { Street = street; City = city; State = state; Country = country; ZipCode = zipcode; } protected override IEnumerable<object> GetEqualityComponents() { // Using a yield return statement to return each element one at a time yield return Street; yield return City; yield return State; yield return Country; yield return ZipCode; } }
در این پیادهسازی Value Object Address هیچ هویتی ندارد و بنابراین هیچ فیلد ID برای آن تعریف نشده است، چه در تعریف کلاس Address یا در تعریف کلاس ValueObject.
نداشتن فیلد ID در یک کلاس برای استفاده توسط Entity Framework (EF) تا قبل از EF Core 2.0 امکان پذیر نبود، اما از نسخه 2 به بعد کمک زیادی به پیاده سازی Value Objectها بدون شناسه می کند.
دو نمونه از نوع Address را می توان با استفاده از تمام متدهای زیر مقایسه کرد:
var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052"); var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052"); Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); //True Console.WriteLine(object.Equals(one, two)); // True Console.WriteLine(one.Equals(two)); // TrueConsole.WriteLine(one == two); // True
حتی با وجود برخی شکافها بین الگوی Value Object متعارف در DDD و نوع موجودیت متعلق در EF Core، در حال حاضر بهترین راه برای ذخیره آنها با EF Core 2.0 و بالاتر است.
ویژگی owned entity type اضافه شده در نسخه 2.0 به EF Core به بعد امکان می دهد انواعی را که هویت خود را به صراحت در مدل دامنه تعریف شده ندارند و به عنوان ویژگی، مانند یک value object، در هر یک از موجودیت های شما استفاده می شوند را mapping کنید.
بنابراین Value Object Address به عنوان بخشی از موجودیت Order، به عنوان یک نوع موجودیت متعلق به موجودیت مالک، که در اینجا موجودیت Order است، پیادهسازی میشود. Address یک نوع بدون property شناسه است که در مدل دامنه تعریف شده است. به عنوان یک ویژگی از نوع Order برای تعیین آدرس حمل و نقل برای یک سفارش خاص استفاده می شود.
طبق قرارداد، یک shadow primary key برای نوع Owner ایجاد می شود و با استفاده از table splitting به همان جدول مالک/Owner نگاشت(map) می شود.
توجه به این نکته مهم است که owned types هیچوقت به صورت قراردادی در EF Core کشف نمیشوند، بنابراین باید آنها را به صراحت اعلام کنید.
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration()); //...Additional type configurations }
// OrderEntityTypeConfiguration.cs class public void Configure(EntityTypeBuilder<Order> orderConfiguration) { orderConfiguration.HasKey(o => o.Id); orderConfiguration.OwnsOne(o => o.Address); }
متد orderConfiguration.OwnsOne(o => o.Address) مشخص می کند که ویژگی Address یک نوع متعلق به موجودیت Order است.
در اینجا به متد "OwnsOne" توجه کنید. این همان چیزی است که به EF می گوید که موجودیت "Order" ما می خواهد از یک value object استفاده کند.
در واقع OwnsOne نشان می دهد که value object بخشی از موجودیت است. این همان چیزی است که به Entity Framework اجازه می دهد تا mapping را انجام دهد. طبق قرارداد Entity Framework هنگام اجرای migrations نام جدول را ValueObject_PropertyName میگذارد و هنگام mapping به دنبال آن میگردد. بنابراین در مورد آدرس، ستونهایی با نامهای Address_City، Address_State و غیره به پایان میرسد.
اگر در configهای مربوطه از ToTable استفاده کنیم و یک نام جدول به آن بدهیم می توانیم owned typeها را در جدول جداگانه نگهداری کنیم
builder.OwnsOne(x => x.Address).ToTable("Address");
با این کار یک رابطه یک به یک در دیتابیس ساخته می شود اما در عمکرد owned type تغییری ایجاد نخواهد شد.
میتوانید برای تغییر نام آن ستونها، متد ()Property().HasColumnName را اضافه کنید. در موردی که Address یک public property است، نگاشت ها(mappings) به صورت زیر خواهد بود:
orderConfiguration.OwnsOne(p => p.Address) .Property(p=>p.Street).HasColumnName("ShippingStreet"); orderConfiguration.OwnsOne(p => p.Address) .Property(p=>p.City).HasColumnName("ShippingCity");
این امکان وجود دارد که متد OwnsOne را در بصورت زنجیره ای نگاشت کنید. در مثال فرضی زیر، OrderDetails مالک BillingAddress و ShippingAddress است که هر دو نوع آدرس هستند. سپس OrderDetails متعلق به نوع Order است.
orderConfiguration.OwnsOne(p => p.OrderDetails, cb => { cb.OwnsOne(c => c.BillingAddress); cb.OwnsOne(c => c.ShippingAddress); }); //... //... public class Order { public int Id { get; set; } public OrderDetails OrderDetails { get; set; } } public class OrderDetails { public Address BillingAddress { get; set; } public Address ShippingAddress { get; set; } } public class Address { public string Street { get; set; } public string City { get; set; } }
بیشتر بخوانید: Entities, Value Objects, Aggregates and Roots
بیشتر بخوانید : Implementing DDD - Clean Architecture
بیشتر بخوانید : لایه Domain در طراحی دامنه گرا DDD
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core