پياده سازي  Aggregate در Domin Layer - DDD

ادامه از : Entities, Value Objects, Aggregates and Rootsقبلا و بصورت کامل در لینک بالا مفهوم Aggregates and Roots را با هم مطالعه کردیم.اما به طور خلاصه "Aggregate یک الگو در طراحی Domain-Driven است. یک Aggregate مجموعه ای از اشیاء دامنه است که که از نظر مفهومی به هم تعلق دارند و می توان آنها را به عنوان یک واحد در نظر گرفت. یک مثال ممکن است یک Order و line-items آن باشد، اینها اشیاء جداگانه خواهند بود، اما بهتر است سفارش (همراه با اقلام آن) را به عنوان یک مجموعه واحد/single aggregate در نظر بگیرید." الگوی Aggregate در مورد transactional consistency است. لازم است قبل از ادامه موضوع اصلی یکسری از مفاهیم را با هم مرور کنیم.مفهوم Database consistency سازگاری(درستی) پایگاه داده بیان می کند که فقط داده های معتبر در پایگاه داده نوشته می شود. اگر تراکنشی اجرا شود که قوانین سازگاری پایگاه داده را نقض کند، کل تراکنش برگردانده می شود و پایگاه داده به حالت اولیه خود باز می گردد.مفهوم consistency in transactionاما سازگاری یا درستی در تراکنش که در این اینجا مدنظر ماست چه چیزی را توصیف می کند؟ داده ها در زمان شروع و پایان تراکنش در یک وضعیت درست هستند. برای مثال، در برنامه‌ای که وجوه را از یک حساب به حساب دیگر منتقل می‌کند، ویژگی سازگاری تضمین می‌کند که ارزش کل وجوه در هر دو حساب در شروع و پایان هر تراکنش یکسان است. در transactional consistency یک کلاینت دستوری را بر روی یک سیستم اجرا می کند که در ادامه تمام عملیات مورد نیاز برای حفظ درستی دامنه را در یک تراکنش اجرا می کند. در پاسخی که کلاینت دریافت می کند یا تمام عملیات ها با موفقیت انجام شده اند یا همه شکست خورده اند، هیچ حد وسطی وجود ندارد.همانطور که در تصویر بالا مشخص است یک aggregate به مجموعه ای از اشیاء دامنه اشاره دارد که با هم گروه بندی شده اند تا سازگاری/درستی تراکنش را تضمين کنند. این اشیاء می‌توانند نمونه‌هایی از موجودیت‌ها (که یکی از آنها aggregate root است) به اضافه هر Value Object اضافی باشند.در اینجا transactional consistency به این معنی است که یک aggregate می بایست در پایان یک business action در یک وضعیت صحیح و به روز باشد.اریک ایوانز می گوید Aggregate به مجموعه ای از اشیاء مرتبط گفته می شود که جهت کنترل تغییرات، به عنوان “یک واحد” در نظر گرفته می شوند.هر Aggregate دارای یک Aggregate Root و یک مرز Aggregate Boundary است. مرز Aggregate مشخص می کند که چیزهایی در آن وجود دارند. Aggregate Root نیز یکی از Entity های داخل Aggregate می باشد و تنها شیء می باشد که اشیاء بیرونی می توانند به آن دسترسی داشته و یا به آن اشاره کنند.Two Aggregates, which represent two transactional consistency boundaries.مشکلی که بسیاری با طراحی Aggregateها دارند این است که محدودیت‌های business مربوطه را در نظر نمی‌گیرند تا از نظر تراکنشی سازگار باشند و در عوض آن‌ها را در خوشه‌های بزرگ طراحی می‌کنند که در شکل زير نشان داده شده است. Aggregateها را تا حد امکان کوچک نگه دارید. بسیاری از Aggregateها فقط دارای ویژگی های اولیه هستند و نياز به Aggregateهای فرعی نخواهند داشت. اینها را به عنوان تصمیمات طراحی در نظر بگیرید: Consistency & validity boundary.هزینه عملکرد و حافظه بارگیری و ذخیره Aggregateها (به خاطر داشته باشید که یک Aggregate معمولاً به عنوان یک واحد بارگیری و ذخیره می شود). Aggregateهاي بزرگ‌تر CPU و حافظه بیشتری مصرف می‌کنند.يک طراحی ضعیف از Aggregate مجموعه‌ای از Aggregateها به‌خوبی طراحی‌شده در شکل زير نشان داده شده‌اند. این Aggregateها بر اساس قوانین business واقعی هستند که نیازمند داده‌های خاصی هستند تا در پایان یک تراکنش موفق پایگاه داده به‌روز شوند. اینها از قوانین Aggregate پیروی می کنند، از جمله طراحی Aggregateهاي کوچک.Some well-designed Aggregates that adhere to true consistency rules. اگر فرض BacklogItem و Product وابستگی‌های داده‌ای داشته باشند، این سوال پیش می‌آید که چگونه هر دوی آنها را به‌روزرسانی کنیم. این به قانون دیگر طراحی Aggregate اشاره می کند، استفاده از سازگاری نهایی(eventual consistency) همانطور که در شکل زير نشان داده شده است.When two or more Aggregates have at least some dependencies on updates, use eventual consistencytransactional vs eventual consistencyمرور مفهوم Invariant طراحی Aggregate شامل درک Invariantها است. Invariantها قوانین کسب و کار هستند که همیشه باید رعایت شوند. در DDD، قوانین اعتبارسنجی(validation rules) را می توان به عنوان Invariant در نظر گرفت.موجودیت های دامنه باید همیشه موجودیت های معتبر باشند. تعداد معینی از Invariantها برای یک شی وجود دارد که همیشه باید معتبر باشند با غیر معتبر شدن آنها و یا به عبارتی دیگر نقض آنها آن شی موجودیت نیز در وضعیت نامعتبر قرار می گیرد. به عنوان مثال، یک شی Order همیشه باید مقداری با یک عدد صحیح مثبت باشد، به اضافه نام کالا و قیمت. بنابراین، اجرای Invariantها بر عهده Aggregate root است و یک شی موجودیت نباید بدون معتبر بودن وجود داشته باشد. قوانین Invariant به سادگی به عنوان قرارداد بیان می شوند و استثناها یا اعلان ها در صورت نقض آنها مطرح می شوند.خوب Entity ها و Value Object های مرتبط با توجه به Invariant هایشان در قالب Aggregate ها دسته بندی می شوند. یک Entity به عنوان Aggregate Root تعریف شده و مسئولیت دسترسی به اشیاء داخل Aggregate و همچنین کنترل Invariant ها را برعهده دارد. اشیاء خارجی تنها می توانند به Aggregate Root دسترسی داشته باشند، و از طریق آن اشیا دیگر داخل Aggregate را تغییر دهند. این مکانیزم باعث می شود تا صحت Invariant ها همیشه رعایت شده و فرآیند های نرم افزار به درستی پیاده سازی شوند.نمونه يک پياده سازيpublic class Order : Entity, IAggregateRoot { private DateTime _orderDate; public Address Address { get; private set; } private int? _buyerId; public OrderStatus OrderStatus { get; private set; } private int _orderStatusId; private string _description; private int? _paymentMethodId; private readonly List<OrderItem> _orderItems; public IReadOnlyCollection<OrderItem> OrderItems => _orderItems; protected Order() { /* This constructor is for ORMs to be used while getting the entity from database. * - No need to initialize the Labels collection since it will be overrided from the database. - It's protected since proxying and deserialization tools may not work with private constructors. */ } public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber, string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) { _orderItems = new List<OrderItem>(); _buyerId = buyerId; _paymentMethodId = paymentMethodId; _orderStatusId = OrderStatus.Submitted.Id; _orderDate = DateTime.UtcNow; Address = address; // ...Additional code ... } public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl, int units = 1) { //... // Domain rules/logic for adding the OrderItem to the order // ... var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units); _orderItems.Add(orderItem); } // ... // Additional methods with domain rules/logic related to the Order aggregate // ... }توجه به این نکته مهم است که این یک موجودیت دامنه است که به عنوان یک کلاس POCO پیاده سازی شده است. هیچ وابستگی مستقیمی به Entity Framework Core یا هیچ فريمورک دیگری ندارد. علاوه بر این، کلاس با رابطی به نام IAggregateRoot مشتق شده است و يا به عبارتي تزئين شده است! آن یک اينترفيس خالی است که گاهی اوقات یک اينترفيس نشانگر(marker interface) نامیده می شود، که فقط برای نشان دادن اینکه این کلاس موجودیت یک AggregateRoot است استفاده می شود.هرچند یک marker interface گاهی اوقات به عنوان یک ضد الگو در نظر گرفته می شود. با این حال، این یک روش تمیز برای علامت گذاری یک کلاس است. یک attribute هم می تواند گزینه دیگری برای نشانگر باشد. در هر صورت موضوع ترجیحات است.داشتن AggregateRoot به این معنی است که بیشتر کدهای مربوط به قواعد کسب و کار موجودیت های Aggregate باید به عنوان متدهایی در کلاس AggregateRoot Order پیاده سازی شوند بله درست متوجه شديد Invariantها (به عنوان مثال، AddOrderItem هنگام اضافه کردن یک شی OrderItem به Aggregate). در ضمن همانطور که قبلا گفته شد شما نباید اشیاء OrderItems را به طور مستقل یا مستقیم ایجاد یا به روز کنید. کلاس AggregateRoot باید کنترل و سازگاری هر عملیات به روز رسانی را در برابر موجودیت های فرزند خود حفظ کند.کپسوله کردن داده ها Encapsulate data in the Domain Entities یک مشکل رایج در مدل‌های موجودیت(entity models) این است که آن‌ها collection navigation properties را به‌ صورت public در دسترس عموم قرار مي دهند. این به هر توسعه‌دهنده همکار اجازه می‌دهد تا محتوای آنها را دستکاری کند، که ممکن است قوانین مهم کسب و کار مربوط به آن را دور بزند و احتمالاً شی را در حالت نامعتبر قرار دهد. راه حل این است که دسترسی فقط خواندنی به مجموعه های مرتبط را قرار دهیم و به صراحت متد هایی را ارائه دهیم که راه هایی را تعریف کنند که از طریق آنها کلاينت ها می توانند آنها را دستکاری کنند.در کد بالا ، توجه داشته باشید که بسیاری از ویژگی‌ها فقط خواندنی یا خصوصی هستند و فقط با متدهای کلاس قابل به‌روزرسانی هستند، بنابراین هر به‌روزرسانی، invariant ها در business domain و منطق مشخص شده در متدهای کلاس را در نظر می‌گیرد.به عنوان مثال، با پیروی از الگوهای DDD، نباید از هیچ command handler method/Web API controllers یا application layer class موارد زیر را انجام دهید (در واقع، انجام این کار برای شما بايد غیرممکن باشد):// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR // COMMAND HANDLERS // Code in command handler methods or Web API controllers //... (WRONG) Some code with business logic out of the domain classes ... OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName, pictureUrl, unitPrice, discount, units); //... (WRONG) Accessing the OrderItems collection directly from the application layer // or command handlers myOrder.OrderItems.Add(myNewOrderItem); //...در این مورد، متد Add صرفاً عملیاتی برای افزودن داده ها با دسترسی مستقیم به مجموعه OrderItems است.اگر Aggregate Root را دور بزنید، Aggregate Root نمی تواند invariantها ، validity یا consistency آن را تضمین کند. در نهایت spaghetti code یا transactional script code را خواهید داشت.برای پیروی از الگوهای DDD، موجودیت ها نباید public setters در هیچ property يک موجودیت داشته باشند. تغییرات در یک موجودیت باید با explicit methods با explicit ubiquitous language در مورد تغییری که در آن موجودیت انجام می‌دهند هدایت شود.علاوه بر این، collectionهاي موجود در موجودیت (مانند order items) باید read-only properties باشند. شما باید بتوانید آن را فقط از درون متدهای کلاس Aggregate Root یا متدهای موجودیت فرزند بروزرسانی کنید.همانطور که در کد Aggregate Root Order مشاهده می کنید، همه setters باید private یا حداقل read-only باشند، به طوری که هر عملیاتی در خصوص داده های موجودیت یا موجودیت های فرزند آن باید از طریق متدهایی در کلاس Aggregate Root انجام شود. این امر به جای اجرای transactional script code، به شیوه ای کنترل شده و شی گرا consistency را حفظ می کند.قطعه کد زیر متد مناسب برای کدنویسی کار اضافه کردن یک شی OrderItem به Order aggregate را نشان می دهد.// RIGHT ACCORDING TO DDD--CODE AT THE APPLICATION LAYER OR COMMAND HANDLERS // The code in command handlers or WebAPI controllers, related only to application stuff // There is NO code here related to OrderItem object's business logic myOrder.AddOrderItem(productId, productName, pictureUrl, unitPrice, discount, units); // The code related to OrderItem params validations or domain rules should // be WITHIN the AddOrderItem method. //...عملیات New OrderItem(params) نیز با متد AddOrderItem از Aggregate Root Order کنترل و اجرا خواهد شد. بنابراین، بیشتر منطق یا اعتبار سنجی مربوط به آن عملیات (به ویژه هر چیزی که بر consistency بین موجودیت های فرزند دیگر تأثیر می گذارد) در یک مکان واحد در Aggregate Root خواهد بود. این هدف نهایی الگوی Aggregate Root است.در DDD، شما می خواهید موجودیت را فقط از طریق متدهای موجود در موجودیت (یا سازنده) به روز کنید تا هر گونه invariant و consistency داده ها را کنترل کنید، بنابراین property ها فقط با یک get accessor تعریف می شوند. دسترسی به اعضای Private فقط از داخل کلاس امکان پذیر است. با این حال، یک استثنا وجود دارد: EF Core باید این فیلدها را نیز تنظیم کند (تا بتواند شی را با مقادیر مناسب برگرداند).نگاشت properties بهمراه get accessors به فيلدهاي جدول ديتابيساما Mapping properties به ستون های جدول پایگاه داده مسئولیت domain نیست بلکه بخشی از infrastructure و persistence layer است. هنگامی که از EF Core 1.0 یا بالاتر استفاده می کنید، در DbContext باید property هایی را که فقط با getters تعریف شده اند به فیلدهای واقعی در جدول پایگاه داده نگاشت کنید. این کار با متد HasField از کلاس PropertyBuilder انجام می شود.در مثال کد OrderAggregate قبلی، چندین فیلد خصوصی مانند فیلد _paymentMethodId وجود دارد که هیچ property مرتبطی برای setter يا getter ندارد. آن فیلد همچنین می تواند در منطق Order محاسبه شود، اما باید در پایگاه داده نیز ذخيره شود. بنابراین در EF Core (از نسخه 1.1) راهی برای نگاشت یک فیلد بدون property مرتبط به ستونی در پایگاه داده وجود دارد. modelBuilder.Entity<MyClass>(e => { e.Property<string>(&quot_paymentMethodId&quot).HasColumnName(&quotPaymentMethodId &quot); });بیشتر بخوانید: Entities, Value Objects, Aggregates and Rootsبیشتر بخوانید : Implementing DDD - Clean Architectureبیشتر بخوانید : لایه Domain در طراحی دامنه گرا DDDبیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Corehttps://zarinp.al/farshidazizi