عنوان:

‫هنر استثناهای سفارشی در توسعه نرم‌افزار با NET.: رویکردی بر پایه طراحی دامنه محور (DDD)


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۱۸ ۱۱:۵۰
آدرس: www.dntips.ir
استثناها (Exceptions) ابزاری حیاتی در برنامه‌نویسی هستند که به ما اجازه می‌دهند تا به‌جای متوقف شدن ناگهانی برنامه، خطاهای غیرمنتظره را مدیریت کرده و اطلاعات ارزشمندی در مورد علت رخداد مشکل به دست آوریم. در اکوسیستم NET. و زبان برنامه‌نویسی #C، مجموعه‌ای غنی از انواع استثناهای پیش‌ساخته مانند ArgumentOutOfRangeException یا NullReferenceException وجود دارند که بسیاری از سناریوهای رایج را پوشش می‌دهند. با این حال، تکیه صرف بر این استثناهای عمومی، ما را از مزایای بزرگ استفاده از استثناهای سفارشی (Custom Exceptions) محروم می‌کند. این مقاله به بررسی اهمیت، مزایا و نحوه پیاده‌سازی استثناهای سفارشی در پلتفرم NET.، با تأکید بر اصول طراحی دامنه محور (Domain-Driven Design - DDD)، می‌پردازد.

چرا به استثناهای سفارشی نیاز داریم؟

در یک معماری نرم‌افزاری که بر پایه DDD طراحی شده، هدف اصلی این است که زبان و مفاهیم دامنه (Domain) کسب‌وکار در تمامی لایه‌ها، به‌ویژه در لایه دامنه، منعکس شود. این اصل، در مورد مدیریت خطاها نیز صدق می‌کند. استفاده از استثناهای عمومی، مانند ArgumentOutOfRangeException، هرچند به درستی وقوع یک مشکل را گزارش می‌دهد، اما اطلاعات مربوط به قوانین کسب‌وکار را در قالب رشته‌های متنی (string) پنهان می‌کند که نه تنها قابل درک نیست، بلکه مدیریت و به‌روزرسانی آن‌ها نیز دشوار است.
یک استثنای سفارشی، به ما این امکان را می‌دهد که خطاها را با استفاده از اصطلاحات و زبان مشترک (Ubiquitous Language) دامنه بیان کنیم. برای مثال، در یک اپلیکیشن بانکی، به جای پرتاب یک ArgumentOutOfRangeException عمومی، می‌توانیم یک استثنا با نام گویای DepositAmountMustBePositiveException (استثنای مبلغ واریزی باید مثبت باشد) پرتاب کنیم. این نام به تنهایی، گویای علت اصلی خطا است و به توسعه‌دهنده و حتی کاربران نهایی، اطلاعات دقیقی در مورد آنچه اشتباه پیش رفته، می‌دهد. این رویکرد، باعث می‌شود که خطایابی سریع‌تر و آسان‌تر انجام شود و کد ما خواناتر و قابل نگهداری‌تر باشد.

پیاده‌سازی و مزایای استثناهای سفارشی در #C

ایجاد یک استثنای سفارشی در #C کاری بسیار ساده است. کافی است یک کلاس جدید تعریف کرده و آن را از کلاس پایه Exception یا یکی از کلاس‌های مشتق شده از آن (مانند ArgumentOutOfRangeException) به ارث ببرید. با ارث‌بری از یک کلاس پایه‌ی مناسب، می‌توانیم هم از ویژگی‌های آن استفاده کنیم و هم با اضافه کردن اطلاعات دامنه، به آن معنای بیشتری ببخشیم.

مثال کد: قبل و بعد

برای درک بهتر، مثالی از یک متد Deposit در یک کلاس مربوط به حساب بانکی را در نظر بگیرید که یک قانون کسب‌وکار را بررسی می‌کند:
بدون استثنای سفارشی:
public void Deposit(decimal amount)
{
    if (amount <= 0) 
        throw new ArgumentOutOfRangeException($"Deposit amount must be greater than 0. Given {amount}");
    // انجام عملیات واریز
}
در این حالت، پیام خطا در قالب یک رشته متنی منتقل می‌شود.

با استثنای سفارشی:
ابتدا کلاس استثنای سفارشی را تعریف می‌کنیم. می‌توانیم آن را در یک فایل جداگانه قرار دهیم تا ساختار کد تمیزتر باشد.
public class DepositAmountMustBePositiveException(decimal invalidAmount) 
  : ArgumentOutOfRangeException($"Deposit amount must be greater than 0. Given {invalidAmount}");
سپس، متد Deposit را با استفاده از استثنای سفارشی به‌روزرسانی می‌کنیم:
public void Deposit(decimal amount)
{
    if (amount <= 0) 
        throw new DepositAmountMustBePositiveException(amount);
    // انجام عملیات واریز
}
این تغییر کوچک، مزایای قابل توجهی به همراه دارد:
  • وضوح و خوانایی: نام استثنا به تنهایی گویای علت دقیق خطا است.
  • قابلیت استفاده مجدد: اگر همین قانون در بخش‌های دیگر برنامه نیز استفاده می‌شود، می‌توانیم از همین استثنای سفارشی بهره ببریم و از تکرار کد (Duplication) جلوگیری کنیم.
  • تست‌پذیری بهتر: تست‌های واحد (Unit Tests) می‌توانند به طور دقیق‌تری بر روی نوع استثنای پرتاب شده متمرکز شوند و از خطاهای احتمالی (false positives) جلوگیری کنند.
  • کاهش وابستگی به رشته‌های متنی: با انتقال پیام خطا به داخل کلاس استثنا، مدیریت و به‌روزرسانی آن آسان‌تر می‌شود.

تست استثناهای سفارشی

همانند سایر کلاس‌ها، استثناهای سفارشی نیز باید تست شوند. دو نوع تست اصلی برای آن‌ها وجود دارد:
  • تست داده‌های استثنا: می‌توانیم یک نمونه از کلاس استثنا را ایجاد کرده و اطمینان حاصل کنیم که پیام یا سایر ویژگی‌های آن (مانند مقدار نامعتبر ورودی) به درستی تنظیم شده‌اند.
  • تست کاربرد استثنا: در این نوع تست، ما متد Deposit را با مقادیر نامعتبر فراخوانی می‌کنیم (مثلاً 0 یا -10.55) و مطمئن می‌شویم که دقیقاً استثنای DepositAmountMustBePositiveException پرتاب می‌شود. این کار به ما اطمینان می‌دهد که منطق کسب‌وکار به درستی پیاده‌سازی شده است.


نتیجه‌گیری

با اینکه استفاده از استثناهای پیش‌فرض NET. در بسیاری از موارد کافی به نظر می‌رسد، اما ایجاد استثناهای سفارشی مزایای قابل توجهی در زمینه خوانایی، نگهداری و تست‌پذیری کد به همراه دارد. این رویکرد، به ویژه در پروژه‌های بزرگ و پیچیده که از اصول طراحی دامنه محور پیروی می‌کنند، حیاتی است و به تیم‌های توسعه کمک می‌کند تا خطاها را سریع‌تر تشخیص داده و برطرف نمایند. استثناهای سفارشی به ما اجازه می‌دهند که زبان کسب‌وکار را به طور کامل در مدیریت خطاها منعکس کرده و در نتیجه، سیستمی قوی‌تر و منعطف‌تر ایجاد کنیم. بنابراین، از ایجاد استثناهای سفارشی برای بیان دقیق خطاهای مربوط به دامنه کسب‌وکارتان خجالت نکشید.

نظرات

  • وحید نصیری در ۱۴۰۴/۰۶/۲۴ ۱۰:۳۰
    اعتبارسنجی دامنه (Domain Validation) در معماری پاک و DDD

    در دنیای مدرن توسعه نرم‌افزار، به‌ویژه با رویکردهای معماری پاک (Clean Architecture) و طراحی دامنه-محور (Domain-Driven Design - DDD)، اعتبارسنجی یک مفهوم کلیدی است که باید در جایگاه درست خود قرار گیرد. اگر این کار به‌درستی انجام نشود، با کدهای پیچیده، تکراری و سخت برای نگهداری مواجه خواهیم شد. اعتبارسنجی اساساً در دو لایه اصلی اتفاق می‌افتد:
    • اعتبارسنجی دامنه (Domain Validation): این نوع اعتبارسنجی قلب تپنده برنامه شماست. هدف آن حصول اطمینان از این است که موجودیت‌ها (Entities) و اشیاء مقدار (Value Objects) شما همیشه و در هر شرایطی در یک وضعیت معتبر باقی بمانند. این قوانین، همان قواعد ذاتی کسب‌وکار (Business Invariants) هستند که هرگز نباید نقض شوند.
    • اعتبارسنجی برنامه (Application Validation): این لایه اولین خط دفاعی شماست. وظیفه آن اعتبارسنجی ورودی‌های کاربر (مانند DTOها و Commandها) قبل از رسیدن به منطق اصلی دامنه است. این اعتبارسنجی بیشتر بر روی فرمت داده‌ها، الزامی بودن فیلدها و موارد این‌چنینی تمرکز دارد.


    اصول کلیدی اعتبارسنجی در لایه دامنه

    قبل از ورود به کد، بیایید با چند اصل اساسی آشنا شویم.
    • کپسوله‌سازی قوانین ذاتی (Encapsulate Business Invariants): موجودیت‌ها و اشیاء مقدار باید خودشان مسئول حفظ اعتبارشان باشند. این کار از طریق سازنده‌ها (Constructors) یا متدهای کارخانه‌ای (Factory Methods) انجام می‌شود. اصل مهم این است: ایجاد یک شیء در وضعیت نامعتبر باید غیرممکن باشد.
    • استفاده از الگوهای مشخص: برای بررسی‌های ساده و پرتکرار از Guard Clauseها استفاده کنید. برای قوانین پیچیده‌تر که ممکن است بین چند موجودیت مشترک باشند یا نیاز به ترکیب داشته باشند، الگوی Specification یک راه‌حل عالی است.
    • بازگرداندن نتایج صریح (Explicit Results): به جای پرتاب استثناهای عمومی (Generic Exceptions) برای خطاهای قابل پیش‌بینی (مانند نامعتبر بودن ورودی)، از الگوی Result استفاده کنید. این الگو به وضوح نشان می‌دهد که یک عملیات می‌تواند موفق یا ناموفق باشد و کد را خواناتر می‌کند. استثناها را برای شرایط واقعاً استثنایی و غیرمنتظره نگه دارید.
    • پرهیز از مدل‌های کم‌خون (Anemic Models): بزرگترین آفت در DDD، مدل‌های دامنه کم‌خون هستند؛ کلاس‌هایی که فقط شامل داده (Properties) هستند و هیچ رفتار یا منطقی ندارند. موجودیت‌های شما باید زنده باشند و علاوه بر داده، رفتارها و منطق اعتبارسنجی را نیز در خود جای دهند.

    پیاده‌سازی گام به گام در .NET

    حالا بیایید این اصول را در عمل پیاده‌سازی کنیم.

    ۱. استفاده از Guard Clause برای قوانین ساده

    Guard Clauseها متدهای استاتیک و کوچکی هستند که پیش‌شرط‌ها را بررسی کرده و در صورت عدم رعایت، یک استثنای مشخص پرتاب می‌کنند. این کار از تکرار بلوک‌های if-throw در سراسر کد جلوگیری می‌کند.
    بیایید یک کلاس Guard ساده بسازیم:
    // Domain/Common/Guard.cs
    namespace Domain.Common;
    
    // یک استثنای سفارشی برای خطاهای اعتبارسنجی دامنه
    public class DomainValidationException : Exception
    {
        public DomainValidationException(string message) : base(message) { }
    }
    
    public static class Guard
    {
        public static void AgainstNullOrEmpty(string value, string parameterName)
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new DomainValidationException($"{parameterName} نمی‌تواند خالی باشد.");
        }
    
        public static void AgainstNegative(decimal value, string parameterName)
        {
            if (value < 0)
                throw new DomainValidationException($"{parameterName} نمی‌تواند مقداری منفی داشته باشد.");
        }
    
        public static void AgainstZeroOrNegative(int value, string parameterName)
        {
            if (value <= 0)
                throw new DomainValidationException($"{parameterName} باید مقداری بزرگتر از صفر داشته باشد.");
        }
    }
    حالا از این Guardها در یک Value Object مانند Money استفاده می‌کنیم:
    // Domain/ValueObjects/Money.cs
    using Domain.Common;
    
    namespace Domain.ValueObjects;
    
    // استفاده از record برای تعریف یک شیء مقدار تغییرناپذیر (Immutable)
    public record Money
    {
        public decimal Amount { get; }
        public string Currency { get; }
    
        // سازنده را private می‌کنیم تا کسی نتواند شیء را در وضعیت نامعتبر بسازد
        private Money(decimal amount, string currency)
        {
            Amount = amount;
            Currency = currency;
        }
    
        // از یک متد کارخانه‌ای استاتیک برای ساخت شیء استفاده می‌کنیم
        public static Money Create(decimal amount, string currency)
        {
            // اعمال قوانین با Guard Clause ها
            Guard.AgainstNegative(amount, nameof(amount));
            Guard.AgainstNullOrEmpty(currency, nameof(currency));
            
            // می‌توانیم قوانین پیچیده‌تری هم اضافه کنیم
            if (currency.Length != 3)
                throw new DomainValidationException("کد ارز باید ۳ حرفی باشد.");
    
            return new Money(amount, currency);
        }
    }
    در این مثال، هیچ‌کس نمی‌تواند یک شیء Money با مقدار منفی یا واحد پول خالی بسازد. این قانون برای همیشه در دل دامنه شما حک شده است. 💡

    ۲. متدهای کارخانه‌ای و سازنده‌های خصوصی

    همان‌طور که در مثال Money دیدید، با خصوصی کردن سازنده و ارائه یک متد استاتیک Create، ما کنترل کاملی بر فرآیند ساخت اشیاء داریم. این الگو برای Entities نیز بسیار قدرتمند است.
    // Domain/Entities/Order.cs
    using Domain.Common;
    using Domain.ValueObjects;
    
    public class Order : Entity // فرض می‌کنیم Entity یک کلاس پایه با یک شناسه (Id) است
    {
        public CustomerId CustomerId { get; private set; }
        public DateTime OrderDate { get; private set; }
        public OrderStatus Status { get; private set; }
    
        private readonly List<OrderItem> _items = [];
        public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    
        // سازنده بدون پارامتر برای استفاده EF Core
        // این سازنده باید private یا protected باشد تا از بیرون فراخوانی نشود
        private Order() { }
    
        public static Order Create(CustomerId customerId, DateTime orderDate)
        {
            // بررسی قوانین ذاتی کسب‌وکار
            if (orderDate > DateTime.UtcNow)
                throw new DomainValidationException("تاریخ سفارش نمی‌تواند در آینده باشد.");
    
            var order = new Order
            {
                Id = Guid.NewGuid(),
                CustomerId = customerId,
                OrderDate = orderDate,
                Status = OrderStatus.Pending
            };
    
            return order;
        }
    
        // مثال دیگری از منطق تجاری درون موجودیت
        public void AddItem(ProductId productId, int quantity, Money price)
        {
            // یک قانون: نمی‌توان به سفارشی که نهایی شده، آیتم اضافه کرد
            if (Status == OrderStatus.Confirmed || Status == OrderStatus.Shipped)
                throw new DomainValidationException("امکان افزودن آیتم به سفارش نهایی‌شده وجود ندارد.");
                
            Guard.AgainstZeroOrNegative(quantity, nameof(quantity));
    
            var existingItem = _items.FirstOrDefault(i => i.ProductId == productId);
            if (existingItem != null)
            {
                // اگر آیتم قبلاً وجود داشته، تعدادش را افزایش بده
                existingItem.AddQuantity(quantity);
            }
            else
            {
                _items.Add(OrderItem.Create(this.Id, productId, quantity, price));
            }
        }
    }

    ۳. الگوی Specification برای قوانین پیچیده

    گاهی قوانین کسب‌وکار پیچیده‌تر از یک بررسی ساده هستند. ممکن است به وضعیت چند Property یا حتی ارتباط با موجودیت‌های دیگر بستگی داشته باشند. در این مواقع، الگوی Specification به ما کمک می‌کند تا این قوانین را در کلاس‌های جداگانه و قابل استفاده مجدد تعریف کنیم.
    ابتدا یک اینترفیس برای Specification تعریف می‌کنیم:
    // Domain/Specifications/ISpecification.cs
    namespace Domain.Specifications;
    
    public interface ISpecification<T>
    {
        bool IsSatisfiedBy(T entity);
        string ErrorMessage { get; }
    }
    حالا یک قانون مشخص را پیاده‌سازی می‌کنیم: "یک سفارش برای تایید شدن، باید حداقل یک آیتم داشته باشد."
    // Domain/Specifications/OrderMustHaveItemsSpec.cs
    using Domain.Entities;
    
    public class OrderMustHaveItemsSpec : ISpecification<Order>
    {
        public string ErrorMessage => "سفارش برای تایید شدن باید حداقل یک آیتم داشته باشد.";
    
        public bool IsSatisfiedBy(Order order) => order.Items.Any();
    }
    نحوه استفاده از آن در موجودیت Order:
    // بخشی از کلاس Order
    public void Confirm()
    {
        var spec = new OrderMustHaveItemsSpec();
        if (!spec.IsSatisfiedBy(this))
        {
            throw new DomainValidationException(spec.ErrorMessage);
        }
    
        // قانون دیگر: فقط سفارش در حالت Pending می‌تواند تایید شود
        if (Status != OrderStatus.Pending)
        {
            throw new DomainValidationException("سفارش در وضعیت فعلی قابل تایید نیست.");
        }
        
        Status = OrderStatus.Confirmed;
        // رویداد دامنه (Domain Event) را منتشر کن (اختیاری)
        // AddDomainEvent(new OrderConfirmedEvent(this.Id));
    }

    ۴. الگوی Result برای نتایج صریح (جایگزین بهتر برای استثناها)

    پرتاب استثنا برای خطاهای اعتبارسنجی قابل پیش‌بینی، یک anti-pattern است. استثناها باید برای خطاهای غیرمنتظره و فاجعه‌بار (مانند قطع شدن ارتباط با دیتابیس) استفاده شوند. برای خطاهای کسب‌وکار، الگوی Result بسیار خواناتر و بهتر است.
    بیایید کلاس Result را پیاده‌سازی کنیم:
    // Domain/Common/Result.cs
    namespace Domain.Common;
    
    public class Result
    {
        public bool IsSuccess { get; }
        public bool IsFailure => !IsSuccess;
        public string? Error { get; }
    
        protected Result(bool success, string? error)
        {
            IsSuccess = success;
            Error = error;
        }
    
        public static Result Success() => new(true, null);
        public static Result Failure(string error) => new(false, error);
        
        public static Result<T> Success<T>(T value) => Result<T>.Success(value);
        public static Result<T> Failure<T>(string error) => Result<T>.Failure(error);
    }
    
    
    public class Result<T> : Result
    {
        public T? Value { get; }
    
        protected internal Result(bool success, T? value, string? error) : base(success, error)
        {
            Value = value;
        }
    
        public static Result<T> Success(T value) => new(true, value, null);
        public static new Result<T> Failure(string error) => new(false, default, error);
    }
    حالا متدهای Create و Confirm در کلاس Order را بازنویسی می‌کنیم تا از Result استفاده کنند:
    // بازنویسی متد Create در کلاس Order
    public static Result<Order> Create(CustomerId customerId, DateTime orderDate)
    {
        if (orderDate > DateTime.UtcNow)
            return Result.Failure<Order>("تاریخ سفارش نمی‌تواند در آینده باشد.");
    
        var order = new Order { /* ... تخصیص مقادیر ... */ };
        return Result.Success(order);
    }
    
    // بازنویسی متد Confirm در کلاس Order
    public Result Confirm()
    {
        var spec = new OrderMustHaveItemsSpec();
        if (!spec.IsSatisfiedBy(this))
            return Result.Failure(spec.ErrorMessage);
    
        if (Status != OrderStatus.Pending)
            return Result.Failure("سفارش در وضعیت فعلی قابل تایید نیست.");
        
        Status = OrderStatus.Confirmed;
        return Result.Success();
    }
    کدی که از این متدها استفاده می‌کند، بسیار تمیزتر و خواناتر خواهد بود:
    // در لایه Application
    var createResult = Order.Create(customerId, DateTime.UtcNow);
    
    if (createResult.IsFailure)
    {
        return BadRequest(createResult.Error); // یا هر پاسخ خطای دیگری
    }
    
    var order = createResult.Value;
    order.AddItem(...);
    
    var confirmResult = order.Confirm();
    if (confirmResult.IsFailure)
    {
        return BadRequest(confirmResult.Error);
    }
    
    _orderRepository.Update(order);
    await _unitOfWork.SaveChangesAsync();

    اعتبارسنجی در لایه Application: نقش FluentValidation

    تا اینجا تمام تمرکز ما بر روی لایه دامنه بود. اما ورودی‌های کاربر (DTO/Command) چه می‌شوند؟ اینجاست که کتابخانه‌هایی مانند FluentValidation وارد عمل می‌شوند.
    نکته مهم: هرگز از FluentValidation یا Data Annotations در لایه دامنه خود استفاده نکنید. این ابزارها به لایه Application تعلق دارند.
    هدف اعتبارسنجی در این لایه، بررسی اولیه و سریع ورودی‌هاست.
    // Application/Orders/Commands/CreateOrder/CreateOrderCommandValidator.cs
    using FluentValidation;
    
    public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
    {
        public CreateOrderCommandValidator()
        {
            RuleFor(x => x.CustomerId)
                .NotEmpty().WithMessage("شناسه مشتری الزامی است.");
    
            RuleFor(x => x.Items)
                .NotEmpty().WithMessage("سفارش باید حداقل شامل یک آیتم باشد.");
    
            RuleForEach(x => x.Items).ChildRules(item =>
            {
                item.RuleFor(i => i.Quantity)
                    .GreaterThan(0).WithMessage("تعداد محصول باید بیشتر از صفر باشد.");
                item.RuleFor(i => i.Price)
                    .GreaterThan(0).WithMessage("قیمت محصول باید بیشتر از صفر باشد.");
            });
        }
    }
    چرا اعتبارسنجی دوگانه؟ شاید بپرسید چرا برخی قوانین (مانند خالی نبودن آیتم‌ها) هم در لایه Application و هم در لایه Domain بررسی می‌شوند.
    • Application Validation (Fail Fast): برای ارائه بازخورد سریع به کاربر و جلوگیری از ارسال درخواست‌های نامعتبر به هسته سیستم.
    • Domain Validation (Always Valid): برای تضمین اینکه قوانین اصلی کسب‌وکار تحت هیچ شرایطی (چه از طریق API، چه یک background job یا تست واحد) نقض نمی‌شوند. لایه دامنه شما نباید به لایه Application اعتماد کند.

    تست کردن منطق دامنه

    یکی از بزرگترین مزایای این رویکرد، تست‌پذیری فوق‌العاده آن است. شما می‌توانید قوانین کسب‌وکار را بدون نیاز به دیتابیس، وب سرور یا هر وابستگی خارجی دیگری تست کنید.
    // Tests/Domain/OrderTests.cs
    using Domain.Entities;
    using FluentAssertions;
    using Xunit;
    
    public class OrderTests
    {
        [Fact]
        public void Create_WhenOrderDateIsInFuture_ShouldReturnFailure()
        {
            // Arrange
            var futureDate = DateTime.UtcNow.AddDays(1);
            var customerId = new CustomerId(Guid.NewGuid());
    
            // Act
            var result = Order.Create(customerId, futureDate);
    
            // Assert
            result.IsFailure.Should().BeTrue();
            result.Error.Should().Be("تاریخ سفارش نمی‌تواند در آینده باشد.");
        }
    
        [Fact]
        public void Confirm_WhenOrderHasNoItems_ShouldReturnFailure()
        {
            // Arrange
            var orderResult = Order.Create(new CustomerId(Guid.NewGuid()), DateTime.UtcNow);
            var order = orderResult.Value;
    
            // Act
            var confirmResult = order.Confirm();
    
            // Assert
            confirmResult.IsFailure.Should().BeTrue();
            confirmResult.Error.Should().Contain("حداقل یک آیتم");
        }
    
        [Fact]
        public void AddItem_ToConfirmedOrder_ShouldThrowException()
        {
            // Arrange
            var orderResult = Order.Create(new CustomerId(Guid.NewGuid()), DateTime.UtcNow);
            var order = orderResult.Value;
            order.AddItem(new ProductId(Guid.NewGuid()), 1, Money.Create(100, "USD"));
            order.Confirm();
    
            // Act
            Action action = () => order.AddItem(new ProductId(Guid.NewGuid()), 1, Money.Create(50, "USD"));
    
            // Assert
            action.Should().Throw<DomainValidationException>()
                .WithMessage("امکان افزودن آیتم به سفارش نهایی‌شده وجود ندارد.");
        }
    }
    این تست‌ها بسیار سریع، قابل اعتماد و متمرکز بر منطق کسب‌وکار هستند.

    جمع‌بندی و بهترین شیوه‌ها

    با ترکیب الگوهایی که بررسی کردیم، به یک استراتژی اعتبارسنجی قوی و منسجم دست پیدا می‌کنید که کاملاً با اصول معماری پاک و DDD همخوانی دارد.
    • قوانین ذاتی را در دامنه متمرکز کنید: منطق کسب‌وکار شما باید در قلب سیستم، یعنی لایه دامنه، محافظت شود.
    • از ابزار مناسب استفاده کنید:Guard Clause برای بررسی‌های ساده و Specification برای قوانین پیچیده.
    • نتایج را صریح برگردانید: از الگوی Result برای خطاهای قابل پیش‌بینی کسب‌وکار استفاده کنید و استثناها را برای شرایط واقعاً استثنایی نگه دارید.
    • دامنه را پاک نگه دارید: لایه دامنه شما نباید هیچ وابستگی به فریمورک‌های لایه Application (مانند FluentValidation یا ASP.NET Core) داشته باشد.
    • ورودی‌ها را در لایه Application اعتبارسنجی کنید: از DTOها و Commandها در همان ابتدای خط لوله درخواست (Request Pipeline) محافظت کنید.
    • برای قوانین دامنه تست بنویسید: این تست‌ها تضمین می‌کنند که منطق اصلی کسب‌وکار شما با تغییر کد همچنان معتبر باقی می‌ماند.

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