در این پست از وبسایت پرووید در رابطه با ایجاد اشیا با روش های برتر در Domain Driven Design صحبت خواهیم کرد.
طراحی دامنه محور یا همان Domain Driven Design در واقع درک نیازهای واقعی کسب و کار مشتری است. ما باید درباره دامنه های مختلف مانند بانکداری، مخابرات، زنجیره تامین، مراقبت های بهداشتی و… دانش داشته باشیم. بنابراین در اینجا دامنه به معنی دانش کسب و کار درباره صنعت خاص است. به طور مشابه طراحی دامنه محور تمرکز بیشتری نسبت به نیازهای کسب و کار دارد نه به تکنولوژی. برای شروع نوشتن یک سیستم، باید بدانیم که مشتری چه چیزی را مد نظر دارد، در حین فاز اولیه هیچگاه درباره برنامه نویسی و معماری آن فکر نمیکنید. هدف اصلی شما درک تمام شرایط کسب و کار مشتری و چگونگی مدل دامنه مورد نیاز او است.
بنابراین چیزی که ما به دنبال آن هستیم بحث با مشتری درباره نیازهای کسب و کار او است. Domain Driven Design صرفا بر اساس این فرضیات برای ترسیم کردن نیازهای کسب و کار در مدل دامنه است. طراحی دامنه محور همه چیز درباره چگونگی طراحی مدل دامنه شماست. به این معنا که هر کلاس دامنه باید یک رابطه مستقیم با آنچه که در دامنه کسب و کار است داشته باشد.
آموزش عملی و پروژه محور Domain Driven Design و CQRS سری آموزشی از وبسایت پرووید است که در رابطه با Domain Driven Design و CQRS تنظیم شده است. پس از این دوره ی آموزشی می توانید از آموزش پیاده سازی اگلوی CQRS در سی شارپ و بسته ی آموزش ویدئویی معماری CQRS در نرم افزار مباحث تئوری و کاربردی استفاده کنید.
اگر تا به حال یک DTO را ساخته اید که چندین شی مختلف را در قالب یک شی یکپارچه میکند میتوانیم بگوییم که از مفهوم Aggregate در Domain Driven Design استفاده کرده اید. اما نکته ای که باید از آن اطلاع داشته باشید این است که برای به دست آوردن بهترین مزایای ارائه شده توسط Domain Driven Design قوانینی وجود دارند که باید از آنها تبعیت کنید. اگر این قوانین را در نظر بگیرید مسائلی همچون Performance و Maintainability و Scalability را بدست خواهید آورد.
اگر تجربه برنامه نویسی در ASP.NET MVC را دارید ممکن است عادت به ساختن Model Class هایی داشته باشید که تمامی دادههای مورد نیاز توسط یک View را در خود جای میدهند. در بعضی دیگر از انواع Application ها به این کلاس ها DTO و یا Data Transfer Object می گویند. در فریم ورک ASP.NET MVC برنامه نویس اغلب در درون یک متد این DTO ها را به سمت View ارسال میکند و سپس یک View را از طریق یکی از پارامترهای ورودی که آپدیتهای انجام شده روی این DTO را هندل میکند. در ادامه تکه کدی از فریم ورک ASP.NET MVC برای شما در نظر گرفته شده است.
Public Function UpdateSalesOrder(so As SalesOrder) As ActionResult
...code to assemble the SalesOrder...
Return View("UpdateOrder", so)
End Function
Public Function UpdateSalesOrder(so As SalesOrder) As ActionResult
...code to use the SalesOrder to update the database...
Return View("UpdateOrder", so)
در این کد Action Method ی وجود دارد که الگویی که در جملات قبلی گفتیم را پیاده سازی می کند. برنامه نویس هایی که تجربه کار کردن ASP.NET MVC را دارند این الگو را به راحتی درک میکنند. آنها اغلب از این الگو برای ساختن اشیایی که تعدادی از داده ها را در کنار هم قرار میدهد تا بتواند توسط کلاینت به روز رسانی و return می شود استفاده می کنند.
استفاده از DTO به این شکل بسیار شبیه به مفهوم Aggregate ها در Domain Driven Design میباشد. در Domain Driven Design یک Aggregate کلاسی است که حاوی چندین Object دیگر است و تمامی محتویات لازم برای انجام یک تراکنش تک را دارد. البته در Domain Driven Design قوانینی برای ایجاد کردن Aggregate ها وجود دارد که تبعیت از آن قوانین مزایایی از قبیل Simplicity و Maintainability و Responsiveness و Scalability را حاصل می شود. چنین قوانینی که در ساختن Aggregate ها در Domain Driven Design موجود می باشند برای جلوگیری از به وجود آمدن سیکل CRAP که در یک مقاله دیگر بر روی وبسایت پرووید از آن حرف زده شد مطرح شده اند.
این سیکل باعث می شود که نرم افزار های ما قابلیت نگهداری بسیار پایینی داشته باشند و به جای بهبود آنها مجبور به جایگزینی آنها شویم.
برای مثال در این مقاله من از یک Object با نام SalesOrder استفاده کردم که در دومین صدور قبض یا همان Billing در یک کمپانی مطرح می شود. در این مقاله من شروع به پر کردن جزئیات SalesOrder می کنم به نحوی که قوانین Aggregate ها در Domain Driven Design نقض نشوند.
افزودن Object های بیشتر
در برنامه من یک SalesOrder شبیه یک DTO عمل می کند و حاوی Object های مرتبط با هم هستند. یک آبجکت از نوع Customer به منظور مشخص کردن مشتری مرتبط با آن SalesOrder یک کالکشن از کلاس SalesOrderDetail که حاوی آیتم هایی هست که در آن SalesOrder سفارش داده شده اند و یک کالکشن از کلاس SalesOrderAdjustments که حاوی اطلاعاتی در رابطه با تخفیف هایی هست که می توانند دارد بر روی SalesOrder انجام شوند. با توجه به این موارد می توان کلاس SalesOrder را به شکل زیر تعریف کرد.
Public Class SalesOrder
Public Property Id As String
Public Property CustomerOwner As Customer
Public Property Details As List(of SalesOrderDetails)
Public Property Adjustments As List(of SalesOrderAdjustments)
End Class
از نقطه نظر Domain Driven Design کلاس SalesOrder یک Aggregate Root هست که از اشیای Customer و SalesOrderDetail و SalesOrderAdjustments تشکیل شده است. برای دسترسی پیدا کردن به اشیایی که درون این Aggregate می باشند فقط میتوان از طریق Aggregate Root آن یعنی SalesOrder اقدام کرد.
دقت کنید که نمیتوانیم هر کالکشنی از اشیا را یک Aggregate بدانیم چون تمامی کالکشن ها یک Aggregate Root ندارند و داشتن یک Aggregate Root برای یک Aggregate الزامی است. برای مثال برنامه نویسان در ASP.NET MVC اغلب لیستی از اشیا را به یک View ارسال میکنند اما یک لیست را نمیتوان یک Aggregate دانست چون (علاوه بر موضوعاتی که اینجا به آنها اشاره نمیشود) یک لیست حاوی یک Aggregate Root نیست.
تعریف Root
در Domain Driven Design علت اهمیت Aggregate Root این است که در یک Aggregate وظیفه اطمینان حاصل کردن از اینکه تمامی Entity ها در یک Valid State یا حالت معتبر هستند به عهده Aggregate Root است. این Valid State هم در زمان بازیابی Aggregate از بانک اطلاعاتی و هم در زمانی انجام هر تغییری بر روی آن Aggregate بررسی می شود. بگذارید این قضیه را با یک مثال مطرح کنیم.
یکی از کارهایی که در برنامه نمونه من باید انجام شود قابلیت اضافه کردن و حذف کردن یک شی Adjustment به SalesOrder است. بر اساس اصول Domain Driven Design طراحی ابتدایی Aggregate من بسیار ضعیف است چرا که برنامه نویس میتواند با استفاده از SalesOrder اشیایی از نوع Adjustment را شبیه کد زیر اضافه کند.
Dim so As New SalesOrder("A123")
so.Adjustments.Add(new Adjustment(AdjustmentTypes.CostWaived))
اگر بخواهیم طراحی این Aggregate را بهبود ببخشیم باید Adjustments به عنوان یک Read-Only Property که یک کالکشن می باشد تعریف کنیم. پس از آن که با استفاده از SalesOrder میتوانیم اقدام به اضافه کردن و حذف کردن اشیای از نوع Adjustment کنیم. با داشتن یک Aggregate با این تعریف یک برنامه نویس باید از کد زیر برای اضافه کردن یک Adjustment به شکل زیر عمل کند.
Dim so As New SalesOrder("A123")
so.AddAdjustment(AdjustmentTypes.CostWaived)
با استفاده از کد بالا Aggregate Root می تواند Adjustment هایی که اضافه شده اند را بررسی کرده و اطمینان حاصل کند که تمامی Adjustment ها Validate شده اند. دقت کنید که پیاده سازی چنین کنترلی برای هر کدام از Entity های درون یک Aggregate کار بسیار دشواری میتواند باشد. هر چند که در مثال فعلی ما میتوانیم Adjustment ها را به یک SalesOrder اضافه یا از آن حذف کنیم اما SalesOrderDetail ها قابل به روز رسانی می باشند. مدیریت کردن هر به روز رسانی بر روی SalesOrderDetail از طریق Aggregate Root می تواند کار دشواری باشد. در چنین سناریویی یکی از گزینههای جایگزین این است که Aggregate Root را به عنوان یک نقطه مرکزی برای انجام Validation بر روی بقیه Entity های درون Aggregate در نظر بگیریم. یکی از راههای پیاده سازی چنین کاری تعریف یک پروپرتی به نام IsSaveable بر روی Aggregate Root است که بر اساس Validation ی که بر روی بقیه Entity های Aggregate انجام میدهد مقدار True یا False برمیگرداند. در چنین پیادهسازی Aggregate Root می تواند از SalesOrderDetail برای انجام بعضی از کارهای Validation استفاده کند. لطفاً کد زیر را ببینید.
Dim so As New SalesOrder("A123")
so.Details(0).Quantity = -1
If so.IsSaveable Then
...saving the sales order...
Else
MessageBox.Show("Sales order not valid")
End If
نکتهای که در اینجا باید به آن اشاره کنیم این است که تمامی کلاس ها نیاز به پروپرتی IsSaveable ندارند. در Domain Driven Design یک شی که قابلیت به روز رسانی را دارد یا یک Aggregate Root است یا یا یک عضو از دقیقا یک Aggregate است. کلاس هایی که Aggregate Root هستند باید پروپرتی های Public ی شبیه IsSaveable را داشته باشند در صورتی که کلاسهایی که Aggregate Root نیستند نیازی به اینجور Property ها ندارند. با این وجود ممکن است کلاس هایی که Aggregate Root نیستند این Property ها را به صورت Private تعریف کنند تا بتوانند توسط Aggregate Root برای انجام Validation استفاده شوند.
پس از ایجاد هرگونه تغییری بر روی تجمع SalesOrder ما باید این شی را به یک کلاس Repository بفرستیم و آن کلاس مسئولیت ذخیرهسازی SalesOrder را به عهده بگیرد. اگر من مسئول نوشتن این کلاس Repository بودم حتما برای ذخیره کردن یک Aggregate از IsSaveable بر روی Aggregate Root استفاده می کردم. من از این Property برای تشخیص این که مجاز به ذخیره سازی آن Aggregate هستم یا نه بهره می برم. در واقع به عنوان یک طراح من می توانم یک اینترفیس را تشکیل دهم تا تمامی نقاط مشترک از جمله پروپرتی هایی که درون تمامی Aggregate Root ها مشترک هستند در درون این اینترفیس قرار بگیرند. یکی از این Property ها IsSaveable میباشد که در درون یک اینترفیس با نام IAggregateRoot تعریف شده و تمامی Aggregate های درون برنامه باید این اینترفیس را پیاده سازی کنند. تعریف این اینترفیس در زبان ویژوال بیسیک در شکل زیر آمده است.
Public Interface IAggregateRoot
Public ReadOnly Property IsSaveable As Boolean
End Interface
الزام اینکه یک کلاس به عنوان Aggregate Root باشد یا فقط در درون یک Aggregate مورد استفاده قرار بگیرد چالش هایی در طراحی را به وجود میآورد. بگذارید با یک مثال موضوع را باز کنیم. در دومین ما که مربوط به Billing و یا همان صدور صورتحساب است و در آن قیمت گذاری اتفاق میافتد SalesOrderDetail هیچ وقت در بیرون از SalesOrder که Aggregate آن است استفاده قرار نمیگیرد. بنابراین محدود کردن این کلاس به SalesOrder مشکل خاصی نیست. اما SalesOrderDetail نیازمند یک شی Product است که اطلاعات مربوط به محصولی که خریداری شده را در درون خود داشته باشد. آن شی Product در درون Domain باید بدون بطور مستقل از SalesOrder مورد نیاز است. این موضوع این سوال را ایجاد میکند که درون یک Aggregate خاص چه چیزهایی باید قرار بگیرند و چه چیزهایی نباید قرار بگیرند. در Domain Driven Design یک تعریف دقیق از یک Aggregate این است که یک Aggregate یک مجموعه از اشیایی هست که درون یک تراکنش تحت الشعاع قرار میگیرند. به عبارت دیگر زمانی که قصد Commit کردن تغییرات مربوط به یک Operation خاص را داریم تمامی اشیایی که تحت الشعاع قرار میگیرند باید بخشی از یک Aggregate خاص باشند. البته ممکن است به دیگر اشیا هم رفرنس بزنیم.
در واقع در Domain Driven Design یک Aggregate یک مرز تراکنش است به این معنا که قبل و بعد از یک تراکنش تمامی دادههایی که درون آن Aggregate هستند باید در حالتی Consistent و Up-to-date باشند. نتیجه مستقیم این تعریف از یک Aggregate این است که اشیای بیرون از Aggregate ممکن است در حالت Consistent نباشند. در Domain Driven Design اگر اشیای بیرون از یک Aggregate پس از ایجاد یک تراکنش و کامل شدن آن در حالتی Consistent نباشند مشکلی ندارد.
دقت کنید که می توانیم دو نوع Consistency را تعریف کنیم: Immediate Condistency و Eventual Consistency. در Immediate Condistency فقط اشیا درون یک Aggregate تک در حالت Consistent می باشند اما در Eventual Consistency تمامی اشیا بیرون از یک Aggregate نیز در حالت Consistent هستند. موضوع دیگر اینکه کاملاً توجیه پذیر است که پس از اعمال کردن یک Adjustment. و محاسبه ی قیمت سفارش دومین Accounting گزارش دهد که آن قیمت هنوز قیمت قبلی است و در واقع تغییرات اعمال شده را نبیند. پس از این موضوع می توانیم در قالب یک تراکنش دیگر قیمت یک سفارش را در دومین Accounting و دومین Billing تنظیم کنیم.
بگذارید مثال جالبتری را خدمت شما عرض کنیم. فرض کنید که آدرس مربوط به ارسال کردن سفارش یک مشتری در دومین Customer تغییر کند. چه اتفاقی می افتد اگر آن تغییر در درون SalesOrder بازتاب نشود؟ خوب این موضوع میتواند خیلی بد باشد یا حتی می تواند اصلاً اهمیتی نداشته باشد. زمانی که صورتحساب قرار است محاسبه و صادر شود آدرس ارسال بسته برای مشتری میتواند در حالت Consistent نباشد چرا که این فرآیند مربوط به محاسبه کردن هزینه سفارش است و اینکه آدرس مشتری تغییر کرده است و آدرس قبلی از بین رفته است در محاسبه قیمت سفارش تاثیری ندارد. اما گاهی ممکن است این موضوع کاملاً اهمیت داشته باشد چرا که بعضی از هزینههای ارسال سفارش به مشتری وابسته به آدرس مشتری است. در چنین شرایطی پس از اینکه آدرس مشتری تغییر کرد نیازمند دوباره محاسبه کردن قیمت سفارش هستیم. در واقع Consistency هم به صورت Immediate Condistency و هم Eventual Consistency به دست آورده شود. با توجه به این قانون و این مثال میتوان به این درک رسید که Product بخشی از SalesOrder نیست و در محاسبه هزینه برای یک SalesOrder نیازی به Product نداریم و آن Product بر اساس SalesOrder و قیمت آن تغییر نمی کند.
ساختن Aggregate های کوچکی که تضمین میکنند Immediate Condistency همیشه باید در Domain اعمال شود مزیتهای متعددی دارد. مهمترین مزیت این است که کلاس هایی که از این روش به دست میآیند به اندازه کافی ساده هستند و پیاده سازی و نگهداری آنها کاملاً و به طور راحت قابل انجام است و به هیچ وجه شما را وارد سیکل CRAP نمیکنند. برخی دیگر از مزایای داشتن Aggregate هایی که حاوی تعداد اندکی از اشیا هستند این است که آنها به راحتی به روز رسانی می شوند. Aggregate های کوچکتر به طور موثرتری مقیاس پذیری دارند چرا که حافظه کمتری را مصرف میکنند. زمان پاسخ را بهبود می بخشند چرا که سریعتر بارگذاری میشوند و احتمال اینکه با بقیه Aggregate ها مداخله کنند کمتر است چرا که هر بخشی از آنها که قابل به روز رسانی و یا Updateable است فقط توسط یک تراکنش در هر لحظه مورد استفاده قرار می گیرد.
البته شما نیاز به یک مکانیزم برای پیادهسازی Eventual Consistency هستید. در رابطه با این موضوع در یکی دیگر از پست های وب سایت پرووید صحبت خواهم کرد.
بگذارید این مقاله را با مطرح کردن یک موضوع مهم تمام کنم. همانطور که ممکن است متوجه شده باشید من از واژه اشیا قابل بهروزرسانی و یا Updateable صحبت کردم. این موضوع به این معنی است که ممکن است در درون یک Aggregate اشیایی وجود داشته باشند که قابل به روز رسانی نباشند. اگر در Domain Driven Design کمی مهارت داشته باشید یا حتی مطالعه کرده باشید تفاوت بین Value Object ها و Entity ها را می دانید. حال به نظر شما در دومین Billing یک کلاس Product باید قابل به روز رسانی باشند یا نه. واضح است که در قسمتی از Domain ما کلاس های Product باید قابل به روز رسانی باشند اما در Billing هم چنین چیزی لازم نیست. این موضوع ما را به این نتیجه میرساند که کلاس Product در واقع باید به صورت اشیاء فقط خواندنی در دومین Billing پیادهسازی شوند و در درون SalesOrder قرار بگیرند. این تفاوت بین Entity ها و Value Object ها آنقدر جالب و جذاب است که وارد شدن به آن را در این قسمت انجام نمیدهم و در یک مقاله دیگر از وبسایت پرووید در رابطه با آن صحبت میکنم. به عنوان آخرین موضوع فراموش نکنید که قوانین Domain Driven Design با هدف فراهم کردن نرم افزارهایی که قابلیت مقیاس پذیری بالایی دارند و از لحاظ Performance و قابلیت نگهداری بهتر عمل می کنند و حتی ساده تر هستند تعریف شده اند. نرم افزارهایی که دچار سیکل وحشتناک CRAP نمی شوند.