فرشید عزیزی
فرشید عزیزی
خواندن ۱۱ دقیقه·۳ سال پیش

آیا بهمراه CQRS باید از الگوی Repository استفاده کنیم؟

برخی از توسعه دهندگان بر این عقیده هستند که همیشه باید از الگوی Repository برای انتزاع منطق دسترسی به داده ها استفاده کنید، در حالی که برخی دیگر فکر می کنند اگر از یک ORM مانند EF Core استفاده می کنید غیر ضروری است. پس آیا همچنان باید از آن استفاده کنید؟ پاسخ هم بله و هم خیر است!(پیشنهاد می کنم این لینک را مطالعه کنید)

اگر از CQRS استفاده می کنید، احتمالاً یک Repository برای ایجاد Aggregateها می خواهید. با این حال، برای یک query ممکن است بخواهید فقط داده های مورد نیاز خود را دریافت کنید تا مجموعه ای از Aggregate ها برای ساخت یک view model.

قبلا و در این لینک مفصل به Repository پرداخته شد.

الگوی Repository چیست؟
الگوی Repository یک الگوی طراحی است که داده ها را "از" و "به" لایه های Domain و Data Access (مانند Entity Framework Core / Dapper) واسطه می کند. Repository کلاس هایی هستند که منطق مورد نیاز برای ذخیره یا بازیابی داده ها را پنهان می کنند. بنابراین، برنامه ما به اینکه از چه نوع ORMي استفاده می کنیم اهمیتی نمی دهد، زیرا همه چیز مربوط به ORM در یک لایه Repository مدیریت می شود. این به شما این امکان را می دهد که تفکیک بهتری از نگرانی ها(SoC) داشته باشید.الگوی Repository یکی از الگوهای طراحی به شدت مورد استفاده برای ساخت solution های تمیزتر(cleaner) است.


الگوی CQRS

اغلب توسعه دهندگان از CQRS برای جدا کردن مسیرهای بین نوشتن (commands) و خواندن (queries) به پایگاه داده خود استفاده می کنند. این الگو امکان تعریف دو مسیر مجزا را فراهم می‌کند که هر کدام می‌توانند تصمیم بگیرند که چگونه با پایگاه داده، وابستگی‌هایشان و غیره تعامل داشته باشند. این یک معماری سطح بالا نیست، بلکه فقط تصمیمی است که می‌توانید در بخش‌های مختلف سیستم خود بگیرید.

زمانی که شما شروع به تمرکز جداگانه روی یک command یا یک query می کنید، در نهایت چه اتفاقی
می افتد، منجر به سازماندهی کد شما بر اساس ویژگی ها(features) می شود. یک ویژگی می تواند یک command یا query یا مجموعه ای از آنها باشد.

یک ویژگی، یک use-case یا مجموعه ای از عملکردهای(functionality) تعریف شده در سیستم شما است.

به صورت عام، functionality اکثر پروژه‌های نرم افزاری خلاصه میشود به CRUD، که object‌ها را میسازیم، آن‌ها را میخوانیم و تغییر میدهیم.

این بدان معناست که شما می توانید تعریف کنید که هر command یا query چگونه نگرانی های مختلف را مدیریت می کند، به عنوان مثال، دسترسی به داده ها.

اگر به تعریف الگوی Repository برگردید، برای دسترسی به اشیاء دامنه(domain objects) است.
Domain Object هایی که با هم گروه بندی می شوند به عنوان Aggregate تعریف می شوند. برای تعامل با Aggregate، تمام عملیات توسط یک شی دامنه اصلی که Aggregate Root است، مدیریت می شود.

نمونه رایجی که اغلب استفاده می شود، Order و Line Items است. Order و اقلام آن، اشیاء دامنه ای هستند که یک Aggregate را تشکیل می دهند. Order همان Aggregate Root است. تمام عملیات از طریق Order انجام می شود و هیچ دسترسی مستقیمی به کالاهای Line Items انجام نمی شود.

این بدان معنی است که ما فقط به یک Aggregate برای ایجاد تغییرات نیاز داریم. به عبارت دیگر، یک Aggregate برای Commands مورد نیاز است، نه Queries.

اینکه ما می‌توانیم برای هر Commandی از یک Aggregate استفاده کنیم و به سادگی از یک مدل داده برای هر query استفاده کنیم. ما برای queryها نیازی به Aggregate نداریم زیرا Aggregate ما مسئول تغییرات حالت است.

همچنین، بیشتر اوقات هنگام ایجاد یک Query، می خواهید داده ها به روش خاصی شکل بگیرند. این لزوماً به همه چیز در درون یک Aggregate نیاز ندارد. به همین دلیل، زمانی که فقط به زیرمجموعه ای از داده ها برای Query نیاز دارید، اغلب در واکشی داده ها برای ایجاد Aggregate زیاده روی می کنید!!!

برای نشان دادن این موضوع، در اینجا نمونه کدی آورده شده است. موجودیت سفارش، Aggregate Root است که از یک Repository بازگردانده می شود.

public class Order : BaseEntity, IAggregateRoot { private Order() { // required by EF } public Order(string buyerId, Address shipToAddress, List<OrderItem> items) { Guard.Against.NullOrEmpty(buyerId, nameof(buyerId)); Guard.Against.Null(shipToAddress, nameof(shipToAddress)); Guard.Against.Null(items, nameof(items)); BuyerId = buyerId; ShipToAddress = shipToAddress; _orderItems = items; } public string BuyerId { get; private set; } public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now; public Address ShipToAddress { get; private set; } // DDD Patterns comment // Using a private collection field, better for DDD Aggregate's encapsulation // so OrderItems cannot be added from &quotoutside the AggregateRoot&quot directly to the collection, // but only through the method Order.AddOrderItem() which includes behavior. private readonly List<OrderItem> _orderItems = new List<OrderItem>(); // Using List<>.AsReadOnly() // This will create a read only wrapper around the private list so is protected against &quotexternal updates&quot. // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance) //https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly(); public decimal Total() { var total = 0m; foreach (var item in _orderItems) { total += item.UnitPrice * item.Units; } return total; } } public class OrderItem : BaseEntity { public CatalogItemOrdered ItemOrdered { get; private set; } public decimal UnitPrice { get; private set; } public int Units { get; private set; } private OrderItem() { // required by EF } public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) { ItemOrdered = itemOrdered; UnitPrice = unitPrice; Units = units; } }

نمونه کد دارای یک Query است که از IOrderRepository برای فهرست کردن همه سفارشات برای کاربر وارد شده استفاده می کند.

public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>> { private readonly IOrderRepository _orderRepository; public GetMyOrdersHandler(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, v CancellationToken cancellationToken) { var specification = new CustomerOrdersWithItemsSpecification(request.UserName); var orders = await _orderRepository.ListAsync(specification, cancellationToken); return orders.Select(o => new OrderViewModel { OrderDate = o.OrderDate, OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel() { PictureUrl = oi.ItemOrdered.PictureUri, ProductId = oi.ItemOrdered.CatalogItemId, ProductName = oi.ItemOrdered.ProductName, UnitPrice = oi.UnitPrice, Units = oi.Units }).ToList(), OrderNumber = o.Id, ShippingAddress = o.ShipToAddress, Total = o.Total() }); } }

از آنجایی که ما به Orders Aggregate نیاز نداریم، واقعاً نیازی به استفاده از الگوی Repository نداریم. مزیت استفاده نکردن از Repository این است که می‌توانیم داده‌هایی را که واقعاً برای این مورد نیاز داریم Select کنیم. در این نمونه، در حال استفاده مجدد از OrderViewModel برای استفاده در هنگام فهرست کردن همه سفارش‌ها و همچنین در مسیر دیگری هنگام مشاهده یک سفارش فردی بود.

این استفاده مجدد در واقع مفید نیست زیرا Order Listing page به هیچ یک از Order Items یا آدرس ارسال نیاز ندارد.

در عوض، ما می توانیم نتیجه خود را به طور صریح برای این use case تعریف کنیم و دقیقاً داده های مورد نیاز را واکشی کنیم. مجدداً، این use case به هیچ Order Items، product برای آن order items یاShipping Address نیاز ندارد. Aggregate در حال واکشی و برگرداندن تمام این داده هایی است که ما به آنها نیاز نداریم.

public class GetMyOrdersHandler : IRequestHandler<GetMyOrdersQuery, GetMyOrdersViewModel> { private readonly CatalogContext _db; public GetMyOrdersHandler(CatalogContext db) { _db = db; } public async Task<GetMyOrdersViewModel> Handle(GetMyOrdersQuery request, CancellationToken cancellationToken) { var result = new GetMyOrdersViewModel(); result.Orders = await _db.CustomerOrdersWithItems(request.UserName) .Select(o => new OrderSummaryViewModel { OrderDate = o.OrderDate, OrderNumber = o.Id, Total = o.OrderItems.Sum(x => x.Units * x.UnitPrice), }) .ToArrayAsync(cancellationToken); return result; } }


جمع بندی

وقتی از CQRS استفاده می کنیم،

در سمت command از یک Repository برای ایجاد و برگرداندن یک Aggregate استفاده کنید و به این معنی است که یک Aggregate یک consistency boundary است و مسئول تغییرات حالت است که توسط invariantsها کنترل می شود.

اما در سمت query از آنجایی که ما به Aggregateها نیاز نداریم، واقعاً نیازی به استفاده از الگوی Repository نداریم. مزیت استفاده نکردن از Repository این است که می‌توانیم داده‌هایی را که واقعاً برای این مورد نیاز داریم را Select کنیم.


بیشتر بخوانید : پیاده سازی CQRS با MediatR در Asp.Net Core

بیشتر بخوانید : پیاده سازی الگوی Repository در ASP.NET Core

بیشتر بخوانید : پياده سازي Aggregate در Domin Layer - DDD

بیشتر بخوانید : Implementing DDD - Clean Architecture

بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core


https://zarinp.al/farshidazizi

cqrsrepositoryddddomain driven designasp net core
Software Engineer
شاید از این پست‌ها خوشتان بیاید