عنوان:

‫Domain-Driven Design (DDD)؛ بخش اول: مقدمه و آشنایی با مفاهیم پایه DDD


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۵/۲۷ ۰۸:۱۰
آدرس: www.dntips.ir
"Domain-Driven Design چیست؟ فراتر از کد، درک دنیای واقعی"

به دنیای پیچیده و پویای توسعه نرم‌افزار خوش آمدید! امروزه، ساخت برنامه‌های کاربردی به سادگی گذشته نیست. با افزایش پیچیدگی کسب‌وکارها، مدل‌سازی دقیق و صحیح دامنه اهمیت زیادی پیدا کرده است. در اینجاست که Domain-Driven Design (DDD) به عنوان یک رویکرد قدرتمند برای حل این چالش‌ها وارد میدان می‌شود.

DDD چیست و چرا به آن نیاز داریم؟

تصور کنید که در حال طراحی یک سیستم برای یک فروشگاه آنلاین هستید. احتمالا اولین فکری که به ذهن شما می‌رسد، دیتابیس و جداول آن است. شما جداولی مثل Products، Customers و Orders را ایجاد می‌کنید و سپس منطق برنامه‌نویسی را بر روی این ساختار داده‌ای می‌سازید. این رویکرد، در نگاه اول ساده و منطقی به نظر می‌رسد، اما به تدریج با اضافه شدن نیازمندی‌های پیچیده کسب‌وکار، با مشکلاتی مواجه می‌شوید. این مدل، که به آن Anemic Domain Model (مدل دامنه بی‌جان) می‌گویند، عموماً شامل کلاس‌هایی است که فقط داده‌ها را نگهداری می‌کنند و هیچ منطق تجاری (Business Logic) در خود ندارند. تمام منطق در Service های بیرونی قرار می‌گیرد، که باعث پراکندگی کد و دشواری در نگهداری می‌شود.
بیایید یک مثال واقعی را در نظر بگیریم. تصور کنید یک سفارش (Order) داریم. در یک Anemic Domain Model، کلاس Order فقط شامل ویژگی‌هایی مثل OrderDate, CustomerId و TotalAmount است و منطق مربوط به آن، مثل CalculateTotalPrice(), CancelOrder() و ApplyDiscount()، در یک کلاس جداگانه به نام OrderService قرار می‌گیرد.
Order.cs (Anemic):
public class Order
{
    public int OrderId { get; set; }
    public int CustomerId { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime OrderDate { get; set; }
}

public class OrderService
{
    public void CancelOrder(Order order)
    {
        // Business logic for cancellation
    }
}

حالا به آن با دیدگاه DDD نگاه کنیم. در اینجا، کلاس Order شامل تمام منطق مربوط به خودش است.
Order.cs (Rich):
public class Order
{
    public int OrderId { get; private set; }
    public int CustomerId { get; private set; }
    public decimal TotalAmount { get; private set; }
    public DateTime OrderDate { get; private set; }
    public OrderStatus Status { get; private set; }

    public void Cancel()
    {
        if (this.Status == OrderStatus.Shipped)
        {
            throw new InvalidOperationException("Cannot cancel a shipped order.");
        }
        this.Status = OrderStatus.Canceled;
        // Raise a Domain Event to notify other parts of the system
        AddDomainEvent(new OrderCanceledEvent(this.OrderId));
    }
}
در این مدل، کلاس Orderمسئول رفتار خود است. این یعنی منطق مربوط به لغو سفارش به صورت کپسوله‌شده در داخل خود کلاس قرار دارد و نمی‌توان از بیرون آن را به صورت ناخواسته تغییر داد. این باعث می‌شود کد قابل فهم‌تر، قابل تست‌تر و در برابر تغییرات مقاوم‌تر باشد.

حالا بیایید جور دیگری به قضیه نگاه کنیم. به جای شروع از دیتابیس، بیایید از زبان خود کسب‌وکار شروع کنیم. با متخصصان حوزه صحبت کنید. از آن‌ها بپرسید که مشتری (Customer) چیست؟ محصول (Product) چگونه تعریف می‌شود؟ یک سفارش (Order) چه مراحلی را طی می‌کند؟ به این فرآیند، کشف دامنه (Domain Discovery) می‌گویند. در DDD، ما به دنبال درک عمیق از دنیای واقعی و نحوه عملکرد آن هستیم.
DDD یک جعبه‌ابزار از تکنیک‌ها و الگوها را ارائه می‌دهد تا به ما کمک کند این دنیای پیچیده را به یک مدل نرم‌افزاری دقیق و غنی تبدیل کنیم. در این رویکرد، کلاس‌های ما فقط نگهدارنده داده نیستند، بلکه رفتارها و منطق تجاری را نیز در خود دارند. به این مدل، Rich Domain Model (مدل دامنه غنی) می‌گویند. این مدل، یک نمای نرم‌افزاری از دنیای واقعی است و می‌تواند به طور مستقل از لایه‌های زیرین (مثل دیتابیس) وجود داشته باشد.

چگونه این اتفاق می‌افتد؟

یکی از مهم‌ترین مفاهیم در DDD، Ubiquitous Language (زبان مشترک) است. این زبان، واژگان مشترکی است که توسط توسعه‌دهندگان و متخصصان کسب‌وکار برای توصیف دامنه استفاده می‌شود. این زبان باید در کد، مستندات و مکالمات روزمره جاری باشد.
برای مثال، اگر در دنیای واقعی، مدیر فروش از واژه "مرجوعی" برای فرآیند بازگرداندن کالا توسط مشتری استفاده می‌کند، ما باید همین کلمه را در کد خود نیز به کار ببریم، نه اینکه از واژه‌هایی مثل ReturnItem یا UndoPurchase استفاده کنیم. این زبان مشترک، سوتفاهم‌ها را به حداقل می‌رساند و ارتباط بین تیم فنی و تیم کسب‌وکار را تسهیل می‌کند.

فلسفه DDD چیست؟

فلسفه DDD به ما می‌گوید که قلب تپنده نرم‌افزار، مدل دامنه است. این مدل باید بازتابی دقیق از پیچیدگی‌های کسب‌وکار باشد. DDD به ما کمک می‌کند که این مدل را به صورت متمرکز و مستقل از جزئیات فنی نگه داریم.
به این فکر کنید:
  • چرا باید زمان و انرژی زیادی را صرف درک دقیق کسب‌وکار کنیم؟
  • اگر مدل ما با دنیای واقعی مطابقت نداشته باشد، چه اتفاقی برای نرم‌افزار ما می‌افتد؟
با این دیدگاه، می‌توانیم به DDD به عنوان یک راهنمای نقشه برای پیمایش در اقیانوس پیچیدگی‌های نرم‌افزاری نگاه کنیم. DDD یک چارچوب برنامه‌نویسی یا یک فریم‌ورک نیست، بلکه یک ذهنیت و رویکرد است. این رویکرد به ما کمک می‌کند تا تمرکز اصلی خود را بر روی مهم‌ترین جنبه یک پروژه، یعنی حل مشکلات واقعی کسب‌وکار، قرار دهیم.

در ادامه این دوره، ما با ابزارها و تکنیک‌های این ذهنیت آشنا خواهیم شد و یاد می‌گیریم که چگونه این مفاهیم را در پروژه‌های ASP.NET Core و EF Core پیاده‌سازی کنیم.

نظرات

  • وحید نصیری در ۱۴۰۴/۰۵/۲۸ ۰۹:۲۸
    یک مثال از کاوش در دنیای مدل‌های دامنه: از سادگی تا غنا، همراه با کشف دامنه

    تصور کنید در حال ساخت یک سیستم نرم‌افزاری هستید، مثلاً یک اپلیکیشن مدیریت سفارشات برای یک فروشگاه آنلاین. شما با Entity Framework Core (EF-Core) کار می‌کنید، که ابزار قدرتمندی برای نقشه‌برداری داده‌ها به دیتابیس است. اما لحظه‌ای مکث کنید: آیا تا به حال فکر کرده‌اید که مدل‌های دامنه‌تان – آن کلاس‌هایی که نماینده مفاهیم کسب‌وکار هستند – چقدر “زنده” هستند؟ آیا آن‌ها فقط حامل داده‌اند، یا رفتارهای هوشمندانه‌ای هم دارند که منطق کسب‌وکار را در خود جای داده‌اند؟ بیایید با هم این مسیر را کاوش کنیم، گویی در حال قدم زدن در یک جنگل ناشناخته هستیم، جایی که هر کشف جدیدی سؤالی تازه به ذهن می‌آورد.

    ابتدا، بیایید به یک الگوی رایج نگاهی بیندازیم که شاید بدون اینکه متوجه باشیم، در کدهایمان جا خوش کرده باشد: مدل دامنه کم‌خون/بی‌جان (Anemic Domain Model). فکر کنید به یک کلاس ساده مانند Order در پروژه .NET شما. این کلاس ممکن است چیزی شبیه به این باشد:
    public class Order
    {
        public int Id { get; set; }
        public DateTime OrderDate { get; set; }
        public decimal TotalAmount { get; set; }
        public List<OrderItem> Items { get; set; }
    }

    اینجا، Order فقط یک کیسه داده است – propertyهایی برای نگهداری اطلاعات، بدون هیچ متدی که رفتار واقعی سفارش را توصیف کند. حالا، منطق کسب‌وکار کجاست؟ معمولاً در لایه‌های دیگر، مثل سرویس‌ها: مثلاً یک OrderService که متدی مانند CalculateTotal(Order order) دارد. با EF-Core، این مدل به راحتی به جدول دیتابیس نقشه‌برداری می‌شود، چون ساده است و تمرکز روی CRUD (Create, Read, Update, Delete) عملیات است.

    اما لحظه‌ای تأمل کنید: آیا این رویکرد همیشه کافی است؟ تصور کنید سیستم‌تان پیچیده‌تر شود – مثلاً نیاز به اعتبارسنجی سفارش بر اساس قوانین کسب‌وکار، یا محاسبه تخفیف‌های پویا. اگر همه چیز در سرویس‌ها باشد، مدل دامنه‌تان “کم‌خون/بی‌جان” می‌ماند، یعنی فاقد رفتارهای ذاتی. این می‌تواند منجر به کدهایی پراکنده شود، جایی که منطق کسب‌وکار در لایه‌های مختلف پخش است. آیا تا به حال احساس کرده‌اید که کدتان بیشتر شبیه به یک مجموعه پروسیجر است تا یک مدل واقعی از دنیای واقعی؟ اینجاست که سؤال بعدی مطرح می‌شود: آیا راه بهتری وجود دارد؟
    حالا، بیایید به سمت مقابل طیف حرکت کنیم: مدل دامنه غنی (Rich Domain Model). اینجا، مدل‌ها نه تنها داده نگه می‌دارند، بلکه رفتارها را هم در خود جای می‌دهند. همان کلاس Order را تصور کنید، اما این بار زنده‌تر:
    public class Order
    {
        public int Id { private set; get; }
        public DateTime OrderDate { private set; get; }
        private List<OrderItem> _items = new List<OrderItem>();
        public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    
        public decimal TotalAmount => CalculateTotal();
    
        public Order(DateTime orderDate)
        {
            OrderDate = orderDate;
        }
    
        public void AddItem(OrderItem item)
        {
            if (item == null) throw new ArgumentNullException(nameof(item));
            _items.Add(item);
        }
    
        private decimal CalculateTotal()
        {
            return _items.Sum(item => item.Price * item.Quantity);
        }
    
        public void ApplyDiscount(decimal percentage)
        {
            if (percentage < 0 || percentage > 100) throw new ArgumentOutOfRangeException(nameof(percentage));
            // منطق اعمال تخفیف
        }
    }

    می‌بینید؟ حالا Order نه تنها داده دارد، بلکه متدهایی برای افزودن آیتم، محاسبه مجموع، یا اعمال تخفیف – همه در جایی که منطقاً باید باشد: درون خود مدل دامنه. با EF-Core، این کار کمی پیچیده‌تر است؛ مثلاً باید از private setters استفاده کنید یا constructorهای خاص تعریف کنید تا ORM بتواند کار کند. اما پاداشش چیست؟ کدی که بیشتر به زبان کسب‌وکار نزدیک است، آسان‌تر برای نگهداری، و کمتر مستعد خطا. آیا این شما را به فکر نمی‌اندازد که چقدر مدل‌های غنی می‌توانند سیستم را انعطاف‌پذیرتر کنند، به خصوص وقتی قوانین کسب‌وکار تغییر می‌کنند؟

    اما چرا این تفاوت‌ها مهم‌اند؟ اینجاست که وارد قلمرو Domain-Driven Design (DDD) می‌شویم، جایی که تمرکز روی درک عمیق دامنه کسب‌وکار است. DDD ما را دعوت می‌کند تا فراتر از کد فکر کنیم: دامنه چیست؟ چگونه مفاهیم واقعی را مدل کنیم؟ حالا، به کشف دامنه (Domain Discovery) فکر کنید – فرآیندی که مثل یک ماجراجویی است برای نقشه‌برداری از دامنه. تصور کنید با ذی‌نفعان کسب‌وکار جلسه‌ای دارید: از تکنیک‌هایی مثل Event Storming استفاده می‌کنید، جایی که رویدادهای کلیدی (مثل “سفارش ثبت شد” یا “پرداخت تأیید شد”) را فهرست می‌کنید، سپس روابط و مرزها را کشف می‌کنید.

    چرا این کشف حیاتی است؟ چون بدون آن، ممکن است ناخواسته به سمت مدل کم‌خون/بی‌جان بروید – ساده، اما ناکافی برای دامنه‌های پیچیده. کشف دامنه به شما کمک می‌کند تا بفهمید کجا رفتارها باید در مدل غنی قرار گیرند. مثلاً در یک سیستم بانکی، آیا “انتقال وجه” فقط یک عملیات سرویس است، یا باید بخشی از رفتار حساب بانکی باشد؟ با EF-Core، این کشف می‌تواند به طراحی entityهایی منجر شود که هم با دیتابیس سازگارند و هم غنی از منطق. آیا تا به حال در پروژه‌ای بوده‌اید که عدم درک دامنه منجر به بازنویسی کد شده؟ کشف دامنه مثل یک چراغ قوه است که مسیر را روشن می‌کند، و شما را تشویق می‌کند تا سؤال بپرسید: “این مفهوم در دنیای واقعی چطور کار می‌کند؟”

    در نهایت، انتخاب بین مدل کم‌خون و غنی بستگی به پیچیدگی دامنه دارد. اگر سیستم‌تان ساده است، مدل کم‌خون/بی‌جان با EF-Core سریع و کارآمد است. اما برای دامنه‌های غنی، مدل غنی – همراه با کشف دامنه در DDD – می‌تواند کد را معنادارتر کند.