CQS و commandها: کاری کنید برنامه هدف کاربر را بفهمد

· 5 دقیقه مطالعه

مهندسی معماری نرم‌افزار #architecture #cqrs #cqs #dotnet #csharp

یک endpoint با نام UpdateInventoryItem می‌فهمد یک موجودیت تغییر کرده است؛ اما لزوماً نمی‌فهمد هدف کاربر چیست.

وقتی تغییر status به قانون‌های کسب‌وکار گره می‌خورد، این تفاوت مهم می‌شود.

موجودیت تغییرکرده، درخواست را پنهان می‌کند

فرض کنید باید یک آیتم موجودی را غیرفعال کنیم. یک API به سبک update شاید چنین ورودی‌ای بگیرد:

UpdateInventoryItem(new InventoryItemDto
{
    Id = itemId,
    Status = InventoryStatus.Deactivated,
    DeactivationReason = reason
});

حالا handler باید از روی فیلدهای تغییرکرده، هدف کاربر را حدس بزند. این توقف موقت است؟ غیرفعال‌سازی با وجود سفارش‌های باز مجاز است؟ دلیل برای هر تغییر status اجباری است یا فقط برای این مورد؟

در یک API رفتارمحور، نام متد خودِ درخواست را بیان می‌کند:

DeactivateInventoryItem(itemId, reason);

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

CQS؛ قرارداد کوچک پشت این ایده

Command–Query Separation یا CQS، عملیات را به دو دسته تقسیم می‌کند:

این یک قرارداد برای متدها و APIها است، نه دستورالعملی برای جدا کردن برنامه به چند سرویس یا چند دیتابیس. فراخوانی مکرر یک query مانند GetInventoryItem باید امن و قابل پیش‌بینی بماند. یک command مانند DeactivateInventoryItem از سیستم می‌خواهد کاری انجام دهد.

قرارداد مربوط به مقدار بازگشتی مفید است، چون فهمیدن رفتار کدِ فراخواننده را ساده‌تر می‌کند. می‌توان query را در شرط به کار برد، آن را ثبت یا دوباره اجرا کرد، بدون اینکه نگران تغییر سیستم باشیم. command به دقت بیشتری نیاز دارد، چون ممکن است درخواست را رد کند یا نتیجه‌ی queryهای بعدی را تغییر دهد.

commandها عمل کسب‌وکار را نام می‌برند

این دو signature را مقایسه کنید:

SetStatus(itemId, InventoryStatus.Deactivated);
DeactivateInventoryItem(itemId, reason);

SetStatus اجازه می‌دهد فراخواننده مقدار یک فیلد را تعیین کند. می‌گوید مقدار نهایی وضعیت چیست، اما نمی‌گوید چرا آن وضعیت معتبر است.

DeactivateInventoryItem یک عمل را بیان می‌کند. نیاز به دلیل را هم صریح می‌کند و برای قانون‌هایی که به غیرفعال‌سازی مربوط‌اند، نه هر تغییر ممکن در status، جای طبیعی پیدا می‌کند. راهنمای CQRS مایکروسافت هم همین تفاوت را مطرح می‌کند: commandها باید کارهای مشخص کسب‌وکار را بیان کنند، نه updateهای سطح پایین داده را.[2]

این به معنی بد بودن هر setter نیست. تغییر locale ترجیحی کاربر می‌تواند یک update ساده باشد. پرسش مفید این است که برنامه باید هدف مشخص کاربر را بفهمد یا فقط یک مقدار را ذخیره کند.

اعتبارسنجی و مجوزدهی کنار command هستند

handler یک command می‌تواند قاعده‌هایی را یک‌جا نگه دارد که در یک update عمومی پراکنده می‌شوند:

public void DeactivateInventoryItem(Guid itemId, string reason, User currentUser)
{
    if (string.IsNullOrWhiteSpace(reason))
        throw new ValidationException("A deactivation reason is required.");

    authorization.Demand(currentUser, InventoryPermissions.Deactivate);

    var item = inventoryItems.Get(itemId);
    item.Deactivate(reason);

    inventoryItems.Save(item);
}

رابط کاربری همچنان می‌تواند دکمه‌ها را غیرفعال کند یا خطاهای واضح را زودتر توضیح دهد. اما مرجع تصمیم نهایی نیست. کلاینت دیگر، retry یا یک درخواست هم‌زمان هم می‌تواند به همان command برسد. قانون باید در command اجرا شود تا همه‌ی فراخواننده‌ها از آن عبور کنند.

این کد صرفاً به‌خاطر commandمحور بودن ارزشمند نیست. مفید است، چون قانون کنار رفتاری قرار دارد که از آن محافظت می‌کند.

command با event یکی نیست

این نام‌ها اغلب شبیه هم هستند، اما چیزهای متفاوتی را توصیف می‌کنند.

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

برای پیاده کردن این ایده، به event bus نیاز نداریم. command می‌تواند یک فراخوانی متد در یک برنامه باشد. اگر بعداً لازم شد مؤلفه‌ی دیگری را باخبر کنیم، command موفق می‌تواند بعد از آن event تولید کند.

هدف کاربر را آشکار کنید

DeactivateInventoryItem(itemId, reason) به‌تنهایی یک مدل دامنه نمی‌سازد. اما باعث می‌شود برنامه کاری را که کاربر خواسته، صریحاً ببیند.

همین کافی است تا اعتبارسنجی، مجوزدهی و قوانین کسب‌وکار جای روشنی داشته باشند. مسیرهای command و query می‌توانند جدا بمانند، بدون اینکه به برنامه‌ها یا دیتابیس‌های جدا نیاز باشد.

Sources

  1. Martin Fowler, "Command Query Separation"
  2. Microsoft Learn, "CQRS pattern"
اشتراک‌گذاری در LinkedIn