یک شی باید قابل تشخیص باشد حتی اگر ویژگی های اشیاء مختلف یکسان باشد. به برنامه ای برای مدیریت دانشجویانی که در دوره های مختلف در یک دانشگاه ثبت نام می کنند فکر کنید. در صورت تغییر ویژگی هایی مانند ایمیل یا حتی نام، یک دانشجو همان دانشجو خواهد بود. مهم این است که تعریف کنیم که یک دانشجو به چه معناست. شاید چیزی شبیه شماره matriculationNumber(شماره دانشجویی) یا فقط یک شناسه عمومی باشد. تصویر زیر یک class diagram برای این برنامه را نشان می دهد
تصویر زیر می تواند نمایش پایگاه داده یک نمونه واقعی از موجودیت یک دانشجو باشد
تفاوت بین موجودیت و کلاس چیست؟
کلاس یک الگو برای یک شی است و یک مفهوم بسیار کلی است. یک موجودیت اهمیت معنایی بیشتری دارد و معمولاً به یک مفهوم گره خورده است (احتمالاً در مورد یک شی واقعی به عنوان مثال، یک کارمند یا یک دانشجو و...) و به business logic مرتبط است.
یک موجودیت به طور کلی به یک جدول در یک پایگاه داده رابطه ای نگاشت می شود.
بسیاری از اشیا هویت مفهومی ندارند. این اشیا ویژگی های یک چیز را توصیف می کنند.
آنها به جای یک شناسه با ویژگی های خود تعریف می شوند. شما می توانید آنها را به عنوان یک ویژگی پیچیده از یک موجودیت در نظر بگیرید.
در واقع Value Object ها اشیایی هستند که تنها توسط ویژگی ها و مقادیرشان شناخته می شوند.
به مثال دانشجو در بخش Entities فکر کنید. دانشجو می تواند آدرسی داشته باشد که شامل نام شهر، خیابان، شماره خیابان، کدپستی است. در دامنه برنامه ما، آدرس یک Value Object خواهد بود. تغییر یکی از ویژگیها باعث میشود که آدرس آن متفاوت باشد. یک آدرس بیشتر شبیه یک نوع ویژگی پیچیده (به جای یک نوع داده اولیه مانند string، bool، و غیره)
اهمیت ندادن به هویت یک شی به ما این آزادی را می دهد که طراحی خود را ساده کرده و عملکرد را بهینه کنیم. اکنون میتوانیم نمونههایی از Value Objectها را به اشتراک بگذاریم زیرا آنها اساساً مانند یک ویژگی پیچیده هستند که برخی اطلاعات را در خود نگه میدارد. اما برای انجام این کار، Value Object باید تغییرناپذیر باشد. تغییر ویژگی های Value Objectها باید منجر به ایجاد یک نمونه جدید و نه تغییر نمونه موجود شود.
باز هم به مثال دانشجویی برگردیم. به دو دانشجو فکر کنید که در یک آدرس زندگی می کنند. آنها میتوانند همان نمونه آدرس Value Object را به اشتراک بگذارند، اما اگر یکی از دانشآموزان به جای دیگری نقل مکان کند، ما نمیخواهیم آن نمونه را تغییر دهیم، زیرا این روی دانشجو دیگر نیز تأثیر میگذارد. در عوض، ما در نهایت یک نمونه آدرس جدید ایجاد می کنیم (یا در صورت وجود از نمونه موجود موجود از آن آدرس استفاده مجدد می کنیم). اشتراک گذاری یک نمونه Value Object در تصویر بعدی نشان داده شده است.
مشخصا Value Objectها نیز در جدول خود در پایگاه داده نشان داده نمی شوند. اگر این کار را انجام دهید، نه تنها مزایای عملکرد و پیچیدگی کمتر Value Objectها را از دست خواهید داد، بلکه مدلی را که تمام اشیاء را به یک قالب وادار می کند به هم می ریزد.
به دلیل مزایای فوق الذکر Value Objectها، شما فقط در صورت نیاز به آن یک هویت اختصاص دهید (و در نتیجه آن را به یک موجودیت تبدیل کنید).
تفاوت در types of equality
برای تعریف تفاوت بین موجودیت ها و Value Objectها، باید سه نوع برابری را معرفی کنیم که زمانی که نیاز به مقایسه اشیاء با یکدیگر داریم، وارد عمل می شوند.
object object1 = new object(); object object2 = object1; bool areEqual = object.ReferenceEquals(object1, object2); // returns true
تفاوت اصلی بین موجودیت ها و Value Objectها در نحوه مقایسه نمونه های آنها با یکدیگر نهفته است. مفهوم برابری شناسه به موجودیت ها اشاره دارد، در حالی که مفهوم برابری ساختاری - برای Value Objectها. به عبارت دیگر، موجودیت ها دارای هویت ذاتی هستند در حالی که Value Object فاقد هویت هستند.
در عمل به این معنی است که Value Objectها دارای یک فیلد شناسه نیستند و اگر دو Value Object دارای مجموعه ای از ویژگی های یکسان باشند، می توانیم آنها را به جای یکدیگر بررسی کنیم. در عین حال، اگر دادهها در دو نمونه موجودیت یکسان باشند (به جز ویژگی Id)، آنها را معادل تلقی نمیکنیم.
شما می توانید آن را به روشی دیگر تصور کنید که در مورد دو نفر با نام های مشابه هستند. به همین دلیل با آنها به عنوان یک فرد رفتار نمی کنید. هر دوی این افراد هویت ذاتی خود را دارند.
با این حال، اگر شخصی یک اسکناس 1 دلاری داشته باشد، برایش مهم نیست که این تکه کاغذ فیزیکی تا زمانی که هنوز 1 دلار ارزش دارد، آن را با یک اسکناس 1 دلاری دیگر جایگزین کند. مفهوم پول در چنین حالتی یک Value Object خواهد بود.
تفاوت در lifespan یا طول عمر
تمایز دیگر بین این دو مفهوم طول عمر نمونه های آنهاست. به اصطلاح، موجودیت ها در پیوستگی زندگی می کنند. آنها تاریخچه ای دارند (حتی اگر آن را ذخیره نکنیم) از آنچه برای آنها اتفاق افتاده است و چگونه آنها در طول زندگی خود تغییر کرده اند.
اما Value Object ها، در عین حال، طول عمر صفر دارند. ما آنها را به راحتی ایجاد و نابود می کنیم. این نتیجه قابل تعویض بودن است. اگر این اسکناس 1 دلاری همان اسکناس دیگر است، چرا زحمت بکشید؟ ما فقط میتوانیم شی موجود را با چیزی که نمونهسازی کردهایم جایگزین کنیم و آن را به کلی فراموش کنیم.
دستورالعملی که از این تمایز ناشی می شود این است که Value Object ها نمی توانند به تنهایی زندگی کنند، آنها باید همیشه به یک یا چند موجودیت تعلق داشته باشند. دادهای که یک Value Object نشان میدهد فقط در ارتباط با موجودیتی که به آن ارجاع میدهد معنا دارد. در مثال بالا با مردم و پول، سوال "چقدر پول؟" منطقی نیست زیرا زمینه مناسبی را بیان نمی کند. در همان زمان، سؤالات "علی چقدر پول دارد؟" یا "همه کاربران ما چقدر پول دارند؟" کاملا معتبر هستند
نتیجه دیگر در اینجا این است که ما Value Object ها را جداگانه ذخیره نمی کنیم. تنها راهی که ما میتوانیم یک Value Object را حفظ کنیم، پیوست کردن آن به یک موجودیت است.
تفاوت در تغییرناپذیری (immutability)
تفاوت بعدی تغییر ناپذیری است. Value Object باید تغییر ناپذیر باشند به این معنا که اگر ما نیاز به تغییر چنین شی ای داشته باشیم، به جای تغییر آن، یک نمونه جدید را بر اساس شی موجود می سازیم. برعکس، موجودیت ها تقریبا همیشه قابل تغییر هستند.
اگر نمیتوانید یک Value Object را تغییرناپذیر کنید، پس آن یک Value Object نیست.
همیشه مشخص نیست که یک مفهوم در مدل دامنه شما یک موجودیت است یا یک Value Object. و متأسفانه، هیچ ویژگی عینی وجود ندارد که بتوانید از آن برای شناخت آن استفاده کنید. اینکه یک مفهوم یک شی Value Object است یا نه کاملاً به حوزه مشکل(کسب و کار مدنظر) بستگی دارد: یک مفهوم می تواند موجودیتی در یک مدل دامنه و یک Value Object در مدل دیگر باشد!!!
در مثال بالا، ما با پول به جای هم رفتار می کنیم که این مفهوم را به یک Value Object تبدیل می کند. در عین حال، اگر نرمافزاری برای ردیابی جریانهای نقدی در کل کشور بسازیم، باید هر قبض را جداگانه بررسی کنیم تا آمار هر یک از آنها جمعآوری شود. در این مورد، مفهوم پول یک موجودیت خواهد بود، اگرچه ما احتمالاً نام آن را Bill میگذاریم.
علیرغم فقدان ویژگی های عینی، هنوز هم می توانید از برخی تکنیک ها برای نسبت دادن یک مفهوم به موجودیت ها یا Value Object استفاده کنید. ما مفهوم هویت را مورد بحث قرار دادیم: اگر میتوانید با خیال راحت نمونهای از یک کلاس را با کلاس دیگری جایگزین کنید که دارای مجموعهای از ویژگیها است، این نشانه خوبی است که این مفهوم یک Value Object است.
فرض کنید در مدل دامنه خود دو کلاس داریم: موجودیت شخص و value object آدرس:
// Entity public class Person { public int Id { get; set; } public string Name { get; set; } public Address Address { get; set; } } // Value Object public class Address { public string City { get; set; } public string ZipCode { get; set; } }
ساختار پایگاه داده در این مورد چگونه خواهد بود؟ یکی از گزینه هایی که به ذهن می رسد این است که برای هر یک از آنها جداول جداگانه ایجاد کنید، مانند این:
چنین طراحی، اگرچه از دیدگاه پایگاه داده کاملاً معتبر است، دو اشکال عمده دارد. اول از همه، جدول آدرس حاوی یک شناسه است. به این معنی که برای کار صحیح با چنین جدولی باید یک فیلد Id جداگانه در Value Object با نام Address معرفی کنیم. این به نوبه خود به این معنی است که ما به کلاس Address هویت خاصی ارائه می کنیم. و این تعریف Value Object را نقض می کند.
اشکال دیگر این است که با این راه حل، ما به طور بالقوه می توانیم Value Objectها را از موجودیت ها جدا کنیم. Value Objectبا نام Address اکنون می تواند به تنهایی زندگی کند زیرا ما می توانیم یک ردیف Person را از پایگاه داده بدون حذف ردیف آدرس مربوطه حذف کنیم. این قانون دیگری را نقض می کند که می گوید طول عمر Value Objectها باید کاملاً به طول عمر موجودیت های اصلی آنها بستگی داشته باشد.
به نظر می رسد که بهترین راه حل این است که فیلدها را از جدول Address در جدول Person قرار دهید، مانند این:
تصویر فوق همه مشکلاتی را که قبلاً بیان کردم حل می کند: آدرس دیگر هویت ندارد و طول عمر آن اکنون کاملاً به طول عمر Person بستگی دارد.
جداول جداگانه را برای Value Objectها معرفی نکنید، فقط آنها را در جدول موجودیت اصلی قرار دهید.
وقتی صحبت از کار با موجودیت ها و Value Objectها به میان می آید، یک دستورالعمل مهم مطرح می شود: همیشه Value Objectها را بر موجودیت ها ترجیح دهید. Value Objectها تغییرناپذیر و سبک تر از موجودیت ها هستند. به همین دلیل، کار با آنها بسیار آسان است. در حالت ایده آل، شما همیشه باید بیشتر منطق کسب و کار را در Value Objectها قرار دهید. موجودیتها در این وضعیت بهعنوان بستهبندی روی آنها عمل میکنند و عملکردهای سطح بالایی را نشان میدهند.
همچنین، ممکن است مفهومی که در ابتدا به عنوان یک موجودیت دیدید اساساً یک Value Object باشد. به عنوان مثال، کلاس Address در کد شما می تواند در ابتدا به عنوان یک موجودیت معرفی شود. ممکن است فیلد ID مخصوص به خود و یک جدول جداگانه در پایگاه داده داشته باشد. پس از بازدید مجدد، ممکن است متوجه شوید که در دامنه شما، آدرس ها در واقع هویت ذاتی خود را ندارند و می توانند به جای یکدیگر استفاده شوند. در این مورد، در اصلاح مدل دامنه خود و تبدیل موجودیت به یک Value Object تردید نکنید.
در طراحی دامنه محور، ساختار Domain Model از موجودیت ها و Value Objectها تشکیل شده است که مفاهیم را در حوزه مشکل نشان می دهد. اما، مدیریت ارتباط بین اشیاء دامنه دلیل اصلی پیچیدگی و سردرگمی است.
اگر طراحی شما هیچ مفهوم روشنی از تکنیکهای سادهسازی نداشته باشد، این associationها(روابط بین اشیاء) ممکن است خارج از کنترل رشد کنند، و اگر مدل شی شما دارای شبکه بزرگی از associationها باشد، اشیاء مرتبط با یک شی ممکن است منجر به بارگیری خوشههای بزرگی از اشیا در حافظه شود(دانلود کل پایگاه داده!!!). حتی اگر در بالاترین سطح سیستم شما، همه این موارد واقعاً به هم مرتبط هستند، ما باید بتوانیم آنها را از هم جدا کنیم تا پیچیدگی سیستم را کنترل کنیم.
یک aggregate مجموعه ای از یک یا چند موجودیت مرتبط و احتمالاً Value Objectها است. هر aggregate دارای یک موجودیت ریشه واحد است که به آن Aggregate Root می گویند. Aggregate Root مسئول کنترل دسترسی به تمام اعضای مجموعه خود است. برای aggregate های تک موجودیتی مشخصا، آن موجودیت خود Aggregate Root آن است. علاوه بر کنترل دسترسی، Aggregate Root وظیفه اطمینان از یکنواختی Aggregate را نیز بر عهده دارد. به همین دلیل مهم است که اطمینان حاصل شود که Aggregate Root مستقیماً فرزندان خود را آشکار نمی کند، بلکه خود دسترسی را کنترل می کند.
هر Aggregate دارای یک ریشه (Aggregate Root) و یک مرز (Aggregate Boundary) است. مرز Aggregate مشخص می کند که چیزهایی در آن وجود دارند. Aggregate Root نیز یکی از Entity های داخل Aggregate می باشد و تنها شیء می باشد که اشیاء بیرونی می توانند به آن دسترسی داشته و یا به آن اشاره کنند.
باید بگویم که Aggregate حیاتی ترین الگو در DDD است و احتمالاً کل طراحی دامنه محور بدون آن مفهومی ندارد.
در حین مطالعه، ممکن است متوجه شوید که Aggregate بیشتر شبیه به مجموعه ای از الگوها است، اما این یک تصور اشتباه است. Aggregate نقطه مرکزی لایه Domain است. بدون آن، دلیلی برای استفاده از DDD وجود ندارد.
هنگام اعمال الگوی aggregate، همچنین مهم است که عملیات persistence(ذخیره و بازیابی اطلاعات) فقط در سطح Aggregate Root اعمال شود. بنابراین، Aggregate Root باید یک موجودیت باشد، نه یک Value Object، تا بتوان آن را "به و از" یک پایگاه داده با استفاده از شناسه آن ادامه داد. این مهم است، زیرا به این معنی است که Aggregate Root میتواند مطمئن باشد که سایر بخشهای سیستم، مستقیما به فرزندانش دسترسی ندارند، آنها را اصلاح نمیکنند و بدون اطلاع او ذخیره نمیکنند. همچنین میتواند روابط بین موجودیتها را سادهتر کند، زیرا معمولاً ویژگیهای ناوبری باید فقط برای انواع درون aggregateها وجود داشته باشد، در حالی که سایر روابط باید فقط با کلید باشند.
وقتی در نظر می گیریم که چگونه موجودیت های خود را در aggregateها ساختار دهید، یک قانون مفید این است که در نظر بگیرید که آیا حذف ها باید آبشاری(cascade) شوند یا خیر. با حذف یک Aggregate Root معمولاً باید همه فرزندان آن نیز حذف شود. اگر متوجه شدید که هنگام حذف ریشه، حذف برخی یا همه فرزندان منطقی نیست، ممکن است لازم باشد در انتخاب ریشه خود تجدید نظر کنید.
یک قاعده کلی، زمانی که چندین شیء به عنوان بخشی از یک تراکنش تغییر می کنند، باید از Aggregate استفاده کنیم.
به عنوان مثال، یک دامنه از یک فروشگاه اینترنتی را در نظر بگیرید که دارای مفاهیمی برای سفارشات است، که در آن یک Order دارای چندین OrderItem است که هر کدام به تعدادی از محصولات خریداری شده اشاره دارد. افزودن و حذف موارد به یک سفارش باید توسط Order کنترل شود. هیچ بخشی از برنامه نباید قادر باشد بدون طی مسیر از Order، یک OrderItem را به عنوان بخشی از یک Order ایجاد کند. با حذف یک سفارش باید تمام OrderItemهای مرتبط با آن نیز حذف شود. بنابراین، Order به عنوان یک Aggregate Root برای OrderItem معنی دارد.
در مورد محصول چطور؟ هر OrderItem تعدادی از یک محصول را نشان می دهد. آیا منطقی است که OrderItem یک ویژگی ناوبری(navigation properties) برای Product داشته باشد؟ اگر چنین است، این امر Order Aggregate را پیچیده میکند. به عنوان یک آزمایش، آیا حذف محصول A در صورت حذف سفارش آن محصول منطقی است؟ قطعاً خیر. بنابراین، محصول به Order Aggregate تعلق ندارد. این احتمال وجود دارد که Product باید Aggregate Root خودش باشد، در این صورت واکشی نمونههای محصول میتواند با استفاده از یک Repository انجام شود. تنها چیزی که برای انجام این کار لازم است شناسه آن است. بنابراین، اگر OrderItem فقط به محصول با شناسه اشاره دارد، کافی است.
یک نگرانی رایج در این مرحله، عملکرد است. اگر OrderItem خاصیت ناوبری برای محصول مرتبط با خود نداشته باشد، چگونه نام محصول در رابط کاربری نمایش یک سفارش نمایش داده می شود؟ در این مورد، این سؤال اشتباه است. سوال بهتر این است که اگر سفارشی برای محصولبا شناسه 123 با نام "Widget A" ثبت شود و در آینده این محصول به "Widget B" تغییر نام دهد، هنگام بررسی این سفارش چه چیزی باید نمایش داده شود؟ به احتمال زیاد، از آنجایی که مشتری احتمالاً اعلانی با جزئیات سفارش خود دریافت کرده است که "Widget A" را درج کرده است (و احتمالاً شناسه آن را اصلاً یادداشت نکرده است)، اگر سیستم اکنون عطف به ماسبق بگوید که "Widget B" را سفارش داده است، باعث سردرگمی خواهد شد!!!!! بنابراین، OrderItem، هنگام ایجاد، احتمالاً باید شامل برخی از جزئیات محصول، مانند نام آن باشد. این به ناچار باعث ایجاد موارد تکراری در سیستم می شود، اما سابقه تاریخی آنچه مشتری خریداری کرده است، حداقل در این سناریو نباید قویا با نام فعلی محصول مرتبط شود. به عنوان یک مزیت جانبی، نمایش Order و OrderItems آن بسیار سریع است، زیرا داده های لازم همه در این مجموعه هستند.
خلاصه Aggregate ها کمک میکنند تا:
چگونه یک Aggregate را تشکیل می دهید؟ طراحی Aggregate شامل درک Invariantها است. Invariantها قوانین کسب و کار هستند که همیشه باید رعایت شوند. درک Invariantهابه طراحی Aggregate شما کمک می کند. Aggregate ها نمونه دیگری از تعریف مرزها(boundaries) بر اساس Invariantها هستند.
در DDD، قوانین اعتبارسنجی(validation rules) را می توان به عنوان Invariant در نظر گرفت. مسئولیت اصلی یک Aggregate ، اجرای Aggregate ها در سراسر تغییرات حالت برای همه موجودیتهای درون آن Aggregate است.
موجودیت های دامنه باید همیشه موجودیت های معتبر باشند. تعداد معینی از Invariantها برای یک شی وجود دارد که همیشه باید معتبر باشند با غیر معتبر شدن آنها و یا به عبارتی دیگر نقض آنها آن شی موجودیت نیز در وضعیت نامعتبر قرار می گیرد. به عنوان مثال، یک شی مورد سفارش همیشه باید مقداری با یک عدد صحیح مثبت باشد، به اضافه نام کالا و قیمت. بنابراین، اجرای Invariantها بر عهده موجودیتهای دامنه (بهویژه Aggregate root) است و یک شی موجودیت نباید بدون معتبر بودن وجود داشته باشد. قوانین Invariant به سادگی به عنوان قرارداد بیان می شوند و استثناها یا اعلان ها در صورت نقض آنها مطرح می شوند.
دلیل این امر این است که بسیاری از اشکالات به این دلیل رخ می دهند که اشیا در حالتی هستند که هرگز نباید در آن قرار می گرفتند.
نکته : Aggregate Root مسئول چک کردن Invariant ها می باشد.
فرض کنید یک SendUserCreationEmailService(سرویسی فرضی برای ارسال ایمیل به یک کاربر) داشته باشیم که یک UserProfile می گیرد. چگونه می توانیم در آن سرویس بررسی کنیم که Name null نیست؟ دوباره چک کنیم؟
امیدوارید که قبلا سیستم در محل مربوطه آن را اعتبارسنجی کرده باشد. البته در TDD یکی از اولین تست هایی که باید بنویسیم این است که اگر برای کاربر نام null بفرستم باید خطایی ایجاد کند. اما وقتی بارها و بارها شروع به نوشتن این نوع تستها میکنیم، متوجه میشویم که ... "اگر هرگز اجازه نمیدادیم نام null شود چه میشد؟ همه این تستها را نداشتیم!".
خوب Entity ها و Value Object های مرتبط با توجه به Invariant هایشان در قالب Aggregate ها دسته بندی می شوند. یک Entity به عنوان Aggregate Root تعریف شده و مسئولیت دسترسی به اشیاء داخل Aggregate و همچنین کنترل Invariant ها را برعهده دارد. اشیاء خارجی تنها می توانند به Aggregate Root دسترسی داشته باشند، و از طریق آن اشیا دیگر داخل Aggregate را تغییر دهند. این مکانیزم باعث می شود تا صحت Invariant ها همیشه رعایت شده و فرآیند های نرم افزار به درستی پیاده سازی شوند.
بیشتر بخوانید :
بیشتر بخوانید : لایه Domain در طراحی دامنه گرا DDD
بیشتر بخوانید : Implementing DDD - Clean Architecture
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core