آموزش عملی و پروژه محور Domain Driven Design و CQRS

آموزش عملی و پروژه محور Domain Driven Design و CQRS

در این پست از وبسایت پرووید در رابطه با آموزش عملی و پروژه محور Domain Driven Design و CQRS صحبت خواهیم کرد. طراحی دامنه محور یا همان Domain Driven Design در واقع درک نیازهای واقعی کسب و کار مشتری است.

Domain Driven Design چیست؟

طراحی دامنه محور یا همان Domain Driven Design در واقع درک نیازهای واقعی کسب و کار مشتری است. ما باید درباره دامنه های مختلف مانند بانکداری، مخابرات، زنجیره تامین، مراقبت های بهداشتی و… دانش داشته باشیم. بنابراین در اینجا دامنه به معنی دانش کسب و کار درباره صنعت خاص است. به طور مشابه طراحی دامنه محور تمرکز بیشتری نسبت به نیازهای کسب و کار دارد نه به تکنولوژی. برای شروع نوشتن یک سیستم، باید بدانیم که مشتری چه چیزی را مد نظر دارد، در حین فاز اولیه هیچگاه درباره برنامه نویسی و معماری آن فکر نمیکنید. هدف اصلی شما درک تمام شرایط کسب و کار مشتری و چگونگی مدل دامنه مورد نیاز او است.

بنابراین چیزی که ما به دنبال آن هستیم بحث با مشتری درباره نیازهای کسب و کار او است. Domain Driven Design صرفا بر اساس این فرضیات برای ترسیم کردن نیازهای کسب و کار در مدل دامنه است. طراحی دامنه محور همه چیز درباره چگونگی طراحی مدل دامنه شماست. به این معنا که هر کلاس دامنه باید یک رابطه مستقیم با  آنچه که در دامنه کسب و کار است داشته باشد.

آموزش عملی و پروژه محور Domain Driven Design و CQRS سری آموزشی از وبسایت پرووید است که در رابطه با Domain Driven Design و CQRS تنظیم شده است. پس از این دوره ی آموزشی می توانید از بسته ی آموزش ویدئویی کاربردی معماری CQRS در نرم افزار و بسته ی آموزش ویدئویی معماری CQRS در نرم افزار مباحث تئوری و کاربردی استفاده کنید.

CQRS چیست؟

به بیان ساده CQRS یک سبک معماری ( Architecture Style) است که با استفاده از آن عملیات خواندن (Read Operations) و عملیات نوشتن (Write Operations) از هم تفکیک می شوند.

در معماری های سنتی و ساده اغلب از یک Data Model یکسان برای Query گرفتن و Update کردن دیتابیس استفاده می شود. این موضوع برای نرم افزارهایی که عملیات ساده ی CRUD را انجام می دهند مناسب است. اما در نرم افزارهای پیچیده تر این روش می تواند مشکل ساز شود. برای مثال، ممکن است عملیات خواندن از دیتابیس شامل انجام Query های پیچیده، کار کردن با DTO ها (Data Transfer Object) و همچنین Object Mapping های پیچیده باشد. از طرفی عملیات نوشتن بر روی دیتابیس می تواند شامل Validation های پیجیده و Business Logic های خاص باشد. بنابراین، اگر در چنین نرم افزارهایی از یک Data Model یکسان برای انجام عملیات خواندن و عملیات نوشتن استفاده شود، Data Model به شدت پیجیده می شود.

در این آموزش نحوه استفاده از Edument CQRS Starter Kit برای ساختن Domain Logic به صورت کامل به همراه تست ها را فرا خواهید گرفت. علاوه بر آن در رابطه با Read Model ها صحبت خواهیم کرد و نحوه استفاده از Domain Logic و Read Model ها در یک برنامه ی نمونه ی ASP.NET MVC بررسی خواهیم کرد. از شما دعوت می‌کنیم که تا پایان این آموزش با وب سایت پرووید همراه شوید و علاوه بر این، از بسته ی آموزش ویدئویی اصول طراحی نرم افزار Domain Driven Design، بسته ی آموزش ویدئویی کاربردی طراحی نرم افزار Domain Driven Design وب سایت استفاده کنید.

در این آموزش نحوه استفاده از Edument CQRS Starter Kit برای ساختن Domain Logic به صورت کامل به همراه تست ها را فرا خواهید گرفت.

علاوه بر آن در رابطه با Read Model ها صحبت خواهیم کرد و نحوه استفاده از Domain Logic و Read Model ها در یک برنامه ی نمونه ی ASP.NET MVC بررسی خواهیم کرد.

از شما دعوت می‌کنیم که تا پایان این آموزش با وب سایت پرووید همراه شوید و علاوه بر این از آموزش Domain Driven Design وب سایت استفاده کنید.

تعریف Domain مسئله

در این آموزش Domain ما یک کافی شاپ خواهد بود و تمرکز ما بر روی مفهومی به نام تب است. وظیفه تب در کافی شاپ ردگیری کردن ملاقات های گروهی از افراد یا افراد تکی به کافی شاپ است. زمانی که افراد به کافی شاپ می آیند و یک میز را انتخاب می‌کنند یک تب باز خواهد شد. پس از آن افراد می‌توانند سفارش نوشیدنی یا غذا بدهند. نوشیدنی ها سریع سرو خواهند شد و این کار توسط کارکنان انجام می‌شود. اما غذا ها باید ابتدا توسط سرآشپز پخته شوند. زمانی که سرآشپز غذا را آماده کرد آن غذا می‌تواند سرو شود.

در زمان ملاقات مشتریان از رستوران ما آنها می توانند نوشیدنیها یا غذای اضافی سفارش بدهند. اگر آنها متوجه شوند که غذایی که سفارش داده اند اشتباه است می توانند سفارش خود را کنسل کنند. البته این موضوع نمی تواند پس از سرو شدن غذا یا نوشیدنی و پذیرفته شدن آن توسط مشتری انجام شود.

در پایان کار مشتری‌ ها هزینه ی اقامت خود در کافی شاپ ما را پرداخت می‌کنند و ممکن است حتی انعامی برای کارکنان در نظر بگیرند. در زمان پرداخت کل هزینه باید پرداخت شود. یک تب که در آن آیتم هایی در حالت غیر سرو شده قرار دارند نمی تواند بسته شود. در واقع تمامی ایتم های درون یک تب یا باید در حالت سرو شده و یا کنسل شده باشند.

تعریف Event ها

در سناریوی که در قسمت بالا معرفی شد افعال و اسم های متنوعی مشخص شدند. زمانی که با روش Database Centric کار میکنیم گوش کردن با دقت به این سناریو ها و مشخص کردن اسم ها و سپس نگاشت کردن آنها با جداول بانک های اطلاعاتی و ایجاد رابطه بین آنها بسیار مرسوم است. در چنین شرایطی افعال اهمیت کمتری مسبت به اسم ها دارند. اما اگر طراحی را بر اساس Command ها و Event ها انجام بدهیم تمرکز بر روی افعال قرار گرفته و اسم ها به عنوان موضوعات کم اهمیت تر در نظر گرفته می شوند. در طراحی Domain تمرکز بر روی افعال قرار میگیرد و نه اسم ها. به عبارت دیگر عملیاتی که یک نرم افزار برای مشتری های خود انجام می دهد بسیار اهمیت دارد و این عملیات در قالب افعال معنا می شوند.

با توجه به سناریوی که در قسمت بالا معرفی شد و تمرکز کردن بر روی زبانی که در آن استفاده می شود ما با کلماتی برخورد می‌کنیم که اشاره به اتفاقاتی دارند که رخ می دهند. این موضوع ما را به سمت اطلاعات جدید در Domain رهنمود می‌شود. ما این اتفاقات را به Event ها نگاشت می کنیم و از آنجایی که Event ها در رابطه با اتفاقاتی هستند که در گذشته رخ داده اند آنها را در با زمان گذشته نامگذاری میکنیم.

در ادامه لیستی از Event هایی که ممکن است در دومین ما مطرح شوند را می بینید:

  • TabOpened
  • DrinksOrdered
  • FoodOrdered
  • DrinksCancelled
  • FoodCancelled
  • DrinksServed
  • FoodPrepared
  • FoodServed
  • TabClosed

نکته بسیار مهمی که در رابطه با Event ها باید به آن اشاره کنیم این است که Event ها با تمرکز کامل بر روی Domain تعریف می شوند.

برای مثال تفاوت بین سفارش دادن نوشیدنی و سفارش دادن غذا در Domain بسیار مهم است بنابراین ما Event های متفاوتی برای آنها در نظر گرفته ایم.

تعریف Command ها

ماهیت Command ها نشانگر یک درخواست به Domain ما میباشند. یکی از نقاط تمایز Event ها و Command ها در این است که یک Event در گذشته بدون شک اتفاق افتاده است اما یک Command چنین نیست. به عبارتی یک Command ممکن است پذیرفته یا رد شود. یک Command پذیرفته شده می‌تواند منجر به رخ دادن هیچ Event یا بیش از چندین Event بشود. این Event ها حقایق جدیدی را وارد سیستم می کنند و اما Command ی که پذیرفته نشده است منجر به نوعی Exception می‌شود.

تعریف Command ها نیز با تمرکز بر روی افعال Domain انجام می شوند. با این وجود آنها به چیزهایی که کاربر از آنها به عنوان یک عملیات و یا Operation یاد می کند دلالت دارند.

برای مثال گر چه که نحوه انجام سفارش های مربوط به نوشیدنی ها و غذاها در Domain ما متفاوت است اما کارمندان نمی‌خواهند غذاها و نوشیدنی های مربوط به یک سفارش یکسان را به صورت جداگانه سرو کنند. سناریوی منطقی تر این است که مشتری غذا و نوشیدنی خود را در کنار هم سفارش دهد و یا حتی پس از مدتی تصمیم به تغییر سفارش خود بگیرد. بنابراین یک Command برای ثبت سفارش وجود خواهد داشت. در ادامه Command هایی که در Domain تعریف می شوند را می بینید.

  • OpenTab
  • PlaceOrder
  • AmendOrder
  • MarkDrinksServed
  • MarkFoodPrepared
  • MarkFoodServed
  • CloseTab

دقت کنید که نام مربوط به این Command ها حاوی افعالی در حالت امری می باشند. این نیز از نقاط تمایز Command ها و Event ها می باشد.

در نامگذاری Command ها از افعالی که در حالت امری هستند استفاده می شود در حالی که در تعریف Event ها از افعالی در حالت گذشته قرار دارند استفاده می شود.

تعریف Exception ها

یکی از قسمت های مهم فرآیند مدل کردن فکر کردن در رابطه با چیزهایی هست که منجر به پذیرفته نشدن یک Command می شود. چنین مواردی را که با نام مسیرهای ناراحت (Sad Paths) نیز معرفی می‌شوند را بصورت Exception ها مدل می کنیم. پیشنهاد می کنیم مفهوم Happy Path و Sad Path را بررسی کنید. دقت کنید که Command ها و Event ها در قالب DTO ها مدل می شوند. این یکی از نقاط تمایز این دو مورد با Exception ها می باشد. Exception ها می توانند حاوی جزئیاتی در رابطه با اینکه چرا Command مورد نظر پذیرفته نشد باشند.

یکی از مزیت های استفاده از Exception ها برای مدل کردن چنین مواردی این است که Front End برنامه باید توسط Domain Logic مطلع شود که چه اتفاق ناخوشایندی رخ داده است. اینکه Front End برنامه با بررسی بعضی از State ها تلاش به کشف کردن مشکل به وجود آمده باشد کند یا حتی کاربر سعی به حدس زدن مشکل به وجود آمده کند روش کاملا اشتباهی است.

با بررسی دقیق سناریو مطرح شده در قسمت های قبلی در رابطه با Domain می توانیم متوجه بشیم که Exception های زیر ممکن است رخ دهند:

  • CannotCancelServedItem
  • TabHasUnservedItems
  • MustPayEnough

دقت کنید که در نامگذاری Exception ها چرایی اینکه Command پذیرفته‌ نشد در نظر گرفته می شود. برای مثال نمیتوان ایتم سرو شده را کنسل کرد و یا تب حاوی ایتم های غیر سرو شده می باشد.

تعریف Aggregate ها

دقت کنید که در Domain افعال به تنهایی نمی‌توانند کار زیادی را انجام بدهند. بدون شک به استفاده از اسم ها هم نیاز داریم. اگر بخواهیم به طور دقیق‌تر بگوییم استفاده از اسم ها برای تعریف کردن State جاری سیستم ضروری هستند. یکی از موارد استفاده از State ها تصمیم گرفتن در رابطه با پذیرفته شدن یا عدم پذیرفته شدن Command ها می باشد. برای مثال در Domain ما برای اینکه Command کنسل کردن یک سفارش پذیرفته شود باید بررسی کنیم و ببینیم آیا State آن سفارش در حالت سرو شده قرار دارد یا نه چرا که در سناریوی که در قسمت های قبلی تعریف کردیم ذکر کردیم که سفارشی که سرو شده باشد و مشتری آن را پذیرفته باشد امکان کنسل شدن را ندارد.

دقت کنید که تمامی اطلاعاتی که ما به آن نیاز داریم در مجموعه‌ Event هایی که رخ داده ‌اند می باشند چرا که آنها تمامی حقایق وارد شده به سیستم را ثبت و ضبط کرده اند. موضوع مهمتر اینکه برای تصمیم گیری در رابطه با پذیرفته شدن یا عدم پذیرفته شدن یک Command نیازی به تمامی Event های رخ داده شده نداریم. در واقع ما فقط به Event هایی که برای تصمیم ‌گیری در رابطه با آن موضوع خاص به ما کمک می کنند اهمیت می دهیم. برای مثال تمامی Event هایی که در رابطه با یک تب (مفهموم تب در Domain کافی شاپ را در قسمت اول این آموزش معرفی کنیم) خاص رخ دادند که با استفاده از آنها می‌توانیم پذیرفته شدن یا عدم پذیرفته شدن Command کنسل کردن سفارش را مشخص کنیم.

با این حساب به مفهوم Aggregate ها میرسیم. هر Aggregate مجموعه Event های خاص خودش را دارد که با در نظر گرفتن تمامی آنها می‌توانیم State فعلی آن Aggregate را مشخص کنیم. Aggregate ها از همدیگر کاملاً تفکیک (Isolated) شده هستند. تصمیم در رابطه با اینکه یک Command باید پذیرفته شود یا نه صرفاً بر اساس خود آن Command و اطلاعات موجود در Event های رخ داده برای یک Aggregate انجام می شود.

به طور کلی Aggregate ها یکی از دو مورد زیر هستند:

  • یک Object که به هیچ Object دیگری رفرنس نمی زند.
  • یک گراف تفکیک شده از Object ها که که یکی از آنها را به عنوان ریشه میشناسیم و دنیای بیرون از آن Aggregate فقط آن ریشه را می شناسند.

طراحی Aggregate ها کار ساده ای نیست چرا که ما را الزام می‌کنند که مفاهیم تجاری (Business Concepts) را کشف و از هم تفکیک کنیم. علاوه بر این موضوع به این معنی است که ما باید به طور کامل بر روی مرزهای سازگاری (Consistency Boundaries) تمرکز کنیم.

در Domain کافی شاپ ما فقط یک Aggregate به نام تب وجود دارد. با این وجود ممکن است در بسیاری از نرم افزارهای دیگر نیاز به کشف Aggregate های بیشتری باشد. به طور کلی آغاز کار از مدل کردن Event ها و Command ها و سپس گروه بندی کردن آن ها بر اساس Invariant ها (قوانین تجاری که باید برقرار باشند) راهکاری مناسب است.

در ادامه کار نوشتن موارد تست (Test Case) را آغاز میکنیم و Domain Logic را میسازیم. همینطور که مشغول به این کار می‌شویم جنبه‌های مختلف طراحی خود را مرور کرده و جزئیات مربوط به Command ها و Event ها را بررسی می کنیم.

پیاده سازی Domain Logic و اولین Command و Event

تمامی سناریوها با باز شدن یک تب آغاز می‌شوند. بنابراین ما در قسمت پایین TabOpened را به عنوان یک Event تعریف می کنیم.

public class TabOpened
{
    public Guid Id;
    public int TableNumber;
    public string Waiter;
}

در این Event پروپرتی هایی برای یک شناسه یکتا برای تبی که باز شده است شماره میز مشتری و همچنین گارسون یا گارسون هایی که مسئول سرو کردن سفارش برای آن تب هستند وجود دارد. کلاس های Event می‌توانند از کلاس Base ی ارث بری کنند. یکی از مهمترین اتفاقاتی که توصیه می‌کنیم برای Event ها بیفتد تعریف یک Id از نوع Guid در آنها است.

تعریف Command مربوط به باز کردن بک تب با نام OpenTab نیز بسیار شبیه به Event بالا می‌باشد. تعریف آن را در قسمت پایین میبینید:

public class OpenTab
{
    public Guid Id;
    public int TableNumber;
    public string Waiter;
}

قرار دادن کلمه ی Event در پایان نام Event ها و یا کلمه ی Command در پایان نام Command ها توصیه نمی‌شود چرا که باعث ایجاد نویز می گردد. نام گذاری مناسب برای Event ها و Command ها می تواند به تنهایی گویای ماهیت آنها باشد. در این راستا توصیه می‌کنیم بسته آموزشی کد نویسی تمیز برای انسان‌ ها را از وبسایت پروید مشاهده کنید.

نوشتن اولین تست

قبل از اینکه بتوانیم تست ها را بنویسیم باید کلاسی را برای Aggregate خود تعریف کنیم. در ادامه تعریف کلاس TabAggregate را مشاهده می کنید.

public class TabAggregate : Aggregate
{
}

همانطور که میبینید این کلاس از کلاس Aggregate ارثدبری کرده است. کلاس Aggregate در بردارنده یک Id و مقداری کد در رابطه با Event های مربوط به Aggregate می باشد.

در این لحظه می توانیم اولین Test Class خود را ساخته و اولین تست خود را در درون آن قرار بدهیم.

[TestFixture]
public class TabTests  : BDDTest<TabAggregate>
{
    private Guid testId;
    private int testTable;
    private string testWaiter;

    [SetUp]
    public void Setup()
    {
        testId = Guid.NewGuid();
        testTable = 42;
        testWaiter = "Derek";
    }

    [Test]
    public void CanOpenANewTab()
    {
        Test(
            Given(),
            When(new OpenTab
            {
                Id = testId,
                TableNumber = testTable,
                Waiter = testWaiter
            }),
            Then(new TabOpened
            {
                Id = testId,
                TableNumber = testTable,
                Waiter = testWaiter
            }));
    }
}

تستی که در قسمت بالا قرار داده شده است می گوید که به شرطی که تاریخچه ‌ی Event ی برای یک تب وجود نداشته باشد زمانی که کامند OpenTab صادر شد ما انتظار داریم که TabOpened تولید شود.

در حال حاضر Solution ما حاوی مواردی است که در تصویر زیر مشاهده می کنید:

solution structure - آموزش عملی و پروژه محور Domain Driven Design و CQRS

اجرای تست و Fail شدن آن

جرای تست نوشته شده در قسمت بالا بدون شک باعث Fail شدن آن می شود اما این شکست جزئیات جالبی را در رابطه با این که در قسمت بعدی باید چه چیزی را پیاده سازی کنیم به ما میدهد.

test 1 - آموزش عملی و پروژه محور Domain Driven Design و CQRS

در جزئیات مربوط به این شکست گفته شده است که TabAggregate که در حال حاضر OpenTab را Handle نمی‌کند.

برای اضافه کردن یک Command Handler جدید باید اینترفیس جنریک IHandleCommand را پیاده سازی کنید و Type Parameter این اینترفیس را با Command ی که قرار است Handle کند تنظیم کنید.

public class TabAggregate : Aggregate,
    IHandleCommand<OpenTab>
{
    public IEnumerable Handle(OpenTab c)
    {
        yield return new TabOpened
        {
            Id = c.Id,
            TableNumber = c.TableNumber,
            Waiter = c.Waiter
        };
    }
}

دقت کنید که پیاده‌سازی Handler برای OpenTab بسیار ساده است. فقط باید TabOpened را ایجاد کنید. علاوه بر این دقت کنید که از کلید واژه yield استفاده شده است تا بتوان صفر یا چندین Event را ساخت. در حال حاضر اجرا کردن تست ما با موفقیت انجام می شود.

test 2 - آموزش عملی و پروژه محور Domain Driven Design و CQRS

ثبت سفارش

برای پیاده سازی فرآیند ثبت سفارش در ابتدا دو Event با نام های DrinksOrdered و FoodOrdered را به صورت زیر تعریف می کنیم.

public class OrderedItem
{
    public int MenuNumber;
    public string Description;
    public bool IsDrink;
    public decimal Price;
}

public class DrinksOrdered
{
    public Guid Id;
    public List<OrderedItem> Items;
}

public class FoodOrdered
{
    public Guid Id;
    public List<OrderedItem> Items;
}

هر دوی این Event ها از کلاس OrderItem به طور اشتراکی استفاده می کنند. دقت کنید که استقلال Event ها از همدیگر بسیار مهم است اما استفاده مجدد از کلاسی به این شکل مشکل ساز نخواهد بود. Command مربوط به ثبت سفارش با نام PlaceOrder شبیه زیر تعریف می شود.

public class PlaceOrder
{
    public Guid Id;
    public List<OrderedItem> Items;
}

دقت کنید که برای نوشتن تست مربوط به این Event ها و Command ها باید در نظر داشت که برای ثبت یک سفارش ابتدا یک تب باید باز باشد. در غیر اینصورت Command پذیرفته نخواهد شد. به همین منظور در ابتدا کلاس TabNotOpen که یک Exception هست را به صورت زیر تعریف می کنیم.

public class TabNotOpen : Exception
{
}

در تعریف Exception های مربوط به یک Aggregate می‌توان هر Exception را در درون یک فایل جداگانه قرار داد. علاوه بر این می‌توان یک فایل تک با نام Exceptions.cs به پروژه اضافه کرد و تمامی Exception های مربوط به یک Aggregate را در آن قرار داد.

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

[Test]
public void CanNotOrderWithUnopenedTab()
{
    Test(
        Given(),
        When(new PlaceOrder
        {
            Id = testId,
            Items = new List<OrderedItem> { testDrink1 }
        }),
        ThenFailWith<TabNotOpen>());
}

بدون شک این تست نیز در ابتدای کار شکست خواهد خورد. در توضیحات مربوط به دلیل شکست این تست گفته شده است که TabAggregate در حال حاضر PlaceOrder در Handle نمی‌کند.

به منظور پاس شدن این تست باید یک Handler برای این Command در نظر بگیریم. برای تعریف یک Handler باید اینترفیس جنریک IHandleCommand را پیاده سازی کنیم. در قسمت های قبلی نحوه پیاده سازی یک Command با استفاده از این اینترفیس را دیدیم.

در قسمت بعدی سه تست بسیار جالب خواهیم نوشت. یک تست برای ثبت سفارشی که فقط حاوی نوشیدنی است یک تست برای سفارشی که فقط حاوی غذا است و یک تست برای سفارشی که حاوی مقداری غذا و همچنین نوشیدنی است.

[Test]
public void CanPlaceDrinksOrder()
{
    Test(
        Given(new TabOpened
        {
            Id = testId,
            TableNumber = testTable,
            Waiter = testWaiter
        }),
        When(new PlaceOrder
        {
            Id = testId,
            Items = new List<OrderedItem>
        }),
        Then(new DrinksOrdered
        {
            Id = testId,
            Items = new List<OrderedItem> { testDrink1, testDrink2 }
        }));
}

[Test]
public void CanPlaceFoodOrder()
{
    Test(
        Given(new TabOpened
        {
            Id = testId,
            TableNumber = testTable,
            Waiter = testWaiter
        }),
        When(new PlaceOrder
        {
            Id = testId,
            Items = new List<OrderedItem> { testFood1, testFood1 }
        }),
        Then(new FoodOrdered
        {
            Id = testId,
            Items = new List<OrderedItem> { testFood1, testFood1 }
        }));
}

[Test]
public void CanPlaceFoodAndDrinkOrder()
{
    Test(
        Given(new TabOpened
        {
            Id = testId,
            TableNumber = testTable,
            Waiter = testWaiter
        }),
        When(new PlaceOrder
        {
            Id = testId,
            Items = new List<OrderedItem> { testFood1, testDrink2 }
        }),
        Then(new DrinksOrdered
        {
            Id = testId,
            Items = new List<OrderedItem> { testDrink2 }
        },
        new FoodOrdered
        {
            Id = testId,
            Items = new List<OrderedItem> { testFood1 }
        }));
}

هدف ما این است که اطمینان حاصل کنیم که آیتم ها در Event های به درستی دسته بندی شوند و سفارش هایی که حاوی غذا و یا نوشیدنی خالی هستند Event ی را ایجاد نکنند.

همه این سه تست در ابتدا با شکست روبرو خواهند شد. در توضیحات علت شکست گفته می شود که TabAggregate نمی ‌داند چطور TabOpened را اعمال کند.

هدف ما این است که اطمینان حاصل کنیم که آیتم ها در Event های به درستی دسته بندی شوند و سفارش هایی که حاوی غذا و یا نوشیدنی خالی هستند Event ی را ایجاد نکنند. همه این سه تست در ابتدا با شکست روبرو خواهند شد. در توضیحات علت شکست گفته می شود که TabAggregate نمی ‌داند چطور TabOpened را اعمال کند.

حال که ما تست هایی داریم که Event هایی در قسمت Given آنها قرار گرفتند یک تاریخچه از Event ها را در دست داریم. Aggregate مورد نظر ما نقش بسیار مهمی در تبدیل کردن این تاریخچه از Event ها به State فعلی سیستم ایفا می‌کند. این نقش بسیار مهم توسط پیاده سازی اینترفیس جنریک IApplyEvent برای هر کدام از Event های مربوطه انجام می شود. متد Apply در درون این اینترفیس باید State یک Aggregate را بر اساس Event و داده های آن به روز رسانی کند. در این مورد خاص محتوای Event برای ما هیچ اهمیتی ندارد.

فقط این موضوع که این Event رخ داده است اهمیت دارد. در قسمت بعد پیاده سازی اینترفیس IApplyEvent را برای TabAggregate را مشاهده می کنید.

public class TabAggregate : Aggregate,
IHandleCommand<OpenTab>,
IHandleCommand<PlaceOrder>,
IApplyEvent<TabOpened>
{
    private bool open = false;

    // Command handlers, omitted for brevity

    public void Apply(TabOpened e)
    {
        open = true;
    }
}

با ایجاد این تغییر هنوز هم تست ها با شکست روبرو می شوند اما یک پیام متفاوت نسبت به قبل دریافت می کنیم. پیام مربوط به شکست تست‌ ها می‌ گوید که Event ها مورد انتظار بودند اما TabNotOpen که یک Exception است رخ داده است.

مرتضی گیتی
بدون نظر

ارسال نظر

نظر
نام
ایمیل
وب سایت