Domain Driven Design - بخش ششم

در این بخش، از سری الگوهایی که کمک می کنند code base بهتری داشته باشیم، به الگوی Command و Query و قابلیت هایی که در سطح application layer اضافه می کند، می پردازیم.Domain Driven Designاولین موضوعی که به آن می پردازیم، الگوی Command و Query است که در سطح application layer می توانیم از قابلیت های آن کمک بگیریم.متدهای لایه application را می توانیم به دو دسته تقسیم کنیم. دسته اول را Write یا Commands نامگذاری می کنیم. هدف آن، نوشتن داده ها و اعمال تغییرات است. دسته دوم را با عنوان Read یا Query می شناسیم که خواندن داده ها برای clientها را مطرح می کند.قوانینی که در بخش های قبل برای aggregateها تعیین شد، باعث می شدند که گرفتن query از سیستم دچار پیچیدگی شود. برای مثال زمانی که قانون reference کردن aggregateها با Id را مطرح می کنیم، کار در query گرفتن سخت می شود. در model driven design، روش های متفاوتی برای query گرفتن وجود دارد. یکی از روش هایی که در DDD مطرح شده است، وجود View Model یا Read Model برای خواندن داده ها است که در این تصویر یک مدل از این جریان را مشاهده می کنید.موضوعی که در command داریم، Invariant Control است. تمام مباحث مانند aggregate, entity و غیره که در بخش های قبل مطرح شدند، برای کنترل invariantهای سیستم بودند. در حالی که در query موضوعی برای کنترل invariantها نداریم. بنابراین یکی از روش هایی که وجود دارد، این است که مکانیزم command و query از هم متفاوت باشد.مفهوم CQRS یا Command Query Responsibility Segregation در سال 2010 مطرح شد و به تفکیک command و query می پردازد. برای مثال در سطح Data Modeling و در یک دیتابیس relational، داده ها را Normalize می کنیم. این کار در حوزه command پیشنهاد می شود. اما برای query، این موضوع مساله ایجاد می کند. بنابراین یکی از حوزه هایی که در آن CQRS مطرح می شود، دیتابیس است. ایده آن این است که داده ها را برای نوشتن در یک دیتابیس ذخیره کنیم و برای خواندن یک data model مناسب query داشته باشیم. سطوح دیگری از CQRS در سطح ساده تری مطرح می شوند. این موضوع که فقط در سطح مدل این کار را انجام دهیم و بصورت فیزیکی دیتابیس ها را از هم جدا نکنیم. به این معنی که برای نوشتن از یک مدل پیروی کنیم و برای خواندن از مدل دیگری که ساده تر است استفاده کنیم. الگویی که در این حوزه به نام Command pattern مطرح می شود، می تواند مزایای زیادی را به ما بدهد. زمانی که در سیستم یک command اتفاق می افتد، آن command از طریق یک interface مشترک، به دست یک command handler می رسد و از این طریق پردازش می شود.public interface ICommandHandler<T> { void Handle(T command); }در نظر بگیرید که به عنوان یک مشتری وارد یک رستوران می شوید. قاعدتا شما برای سفارش غذا مستقیما سراغ آشپز نمی روید و فقط پیش خدمت را می شناسید. پیش خدمت بسته به سفارش شما، سفارش را به دست یکی از آشپزها می رساند و نتیجه را برمی گرداند.در این الگو، اگر clientها را Rest در نظر بگیریم، فقط Command Bus را به عنوان یک گذرگاه می شناسند و از این طریق، commandها به command handler می رسد. اولین موضوعی که به عنوان یک مزیت مطرح می شود، این است که وابستگی ما به command hadlerها صفر می شود و این می تواند اعمال تغییرات را راحت تر کند. بنابراین side effect تغییرات در command handlerها روی clientها تاثیری نمی گذارد.موضوع دوم این است که تمام commandها از یک گذرگاه واحد عبور می کنند. مزیتی که به ما می دهد، برای مثال لاگ تمام commandها را می توانیم داشته باشیم. همچنین کنترل این موضوع که چه کسی می تواند این command را پردازش کند را داشته باشیم.موضوع سوم این است که، interface در تمام اجزایی که command را handle می کنند، یکی می شود. داشتن interface یکسان، یعنی می توانیم به یک چشم با command handlerها برخورد کنیم و همه را در قالب یک abstraction نگاه کنیم. در حوزه decoupling، فرض کنید که یک Web Tier و یک Application Tier داریم که به صورت فیزیکی از هم جدا شدند. web tier از طریق rest درخواست ها را می گیرد و بر روی یک command bus قرار می دهد. bus می تواند commandها را در یک صف قرار دهد و با این تفکیک سازی، می توانیم تعداد application tierها را بالا ببریم تا به صورت موازی به commandها گوش دهند. از command bus به عنوان گذرگاه commandها صحبت کردیم که از طریق یک متد، command را دریافت می کند. clientها فقط command و command bus را می شناسند. در این طراحی، می توانیم از یک اینترفیس به عنوان marker برای commandها استفاده کنیم. از این طریق می توانیم تایپ جنریک ها را محدود به command کنیم.public interface ICommand { } public interface ICommandBus { void Dispatch<T>(T command) where T : ICommand; } public interface ICommandHandler<T> where T : ICommand { void Handle(T command); }فرض کنید در application ما، یک open auction command اتفاق می افتد.public class OpenAuctionCommand : ICommand { public string Product { get; set; } public int StartingPrice { get; set; } public DateTime EndDateTime { get; set; } } public class OpenAuctionHandler : ICommandHandler<OpenAuctionCommand> { public void Handle(OpenAuctionCommand command) { Console.WriteLine(&quotHandling Open Auction&quot); } }کلاس command handler، به مشابه یکی از توابع در application service است و کار پردازش این command را انجام می دهد. در سطح client، فقط commandها و command bus را می شناسیم.var container = BuildContainer(); var bus = container.Resolve<ICommandBus>(); var openAuction = new OpenAuctionCommand { Product = &quotx&quot, StartingPrice = 1000, EndDateTime = DateTime.Now.AddDays(10) }; bus.Dispatch(openAuction);در نهایت command bus کلاس command handler را پیدا می کند و command را به آن ارسال می کند. بهترین حالتی که می توانیم برای پیاده سازی یک In-memory command bus در نظر بگیریم، استفاده از یک ابزار به عنوان IOC Container برای مثال Autofac است که بتواند برای ما Dependency Injection انجام دهد.به این صورت که به Autofac یک assembly معرفی می کنیم و از آن می خواهیم هر کلاسی که ICommandHandler را پیاده سازی کرده است را پیدا کند و register کند.private static IContainer BuildContainer() { var builder = new ContainerBuilder(); builder.RegisterAssemblyTypes(typeof(Program).Assembly) .As(type => type.GetInterfaces() .Where(interfaceType => interfaceType.IsClosedTypeOf(typeof(ICommandHandler<>)))) .InstancePerLifetimeScope(); builder.RegisterType<AutofacCommandBus>() .As<ICommandBus>().SingleInstance(); return builder.Build(); }در پیاده سازی AutofacCommandBus، به Autofac LifetimeScope یک reference داریم. ICommandHanlder مربوطه را از آن دریافت می کند و متد Handle را صدا می زند.public class AutofacCommandBus : ICommandBus { private readonly ILifetimeScope _scope; public AutofacCommandBus(ILifetimeScope scope) { _scope = scope; } public void Dispatch<T>(T command) where T : ICommand { var handler = _scope.Resolve<ICommandHandler<T>>(); handler.Handle(command); } }بنابراین، در حالت In-memory بهترین حالت استفاده از ابزارهای IOC Container می تواند باشد. از این جهت که در command handlerها احتمالا وابستگی وجود دارد و ساخت آن ها موضوعیت پیدا می کند.