← بازگشت

سوالات مصاحبه Domain-Driven Design (DDD) در .NET

مقدمه

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

سوال 1: Domain-Driven Design (DDD) چیست و هدف اصلی آن کدام است؟

پاسخ: Domain-Driven Design (DDD) یک رویکرد توسعه نرم‌افزار است که بر مدل‌سازی دامنه کسب‌وکار (business domain) تمرکز دارد. هدف اصلی DDD این است که پیچیدگی‌های ذاتی یک دامنه کسب‌وکار را درک کرده و آن‌ها را در مدل نرم‌افزاری منعکس کند. این رویکرد توسط Eric Evans در کتاب “Domain-Driven Design: Tackling Complexity in the Heart of Software” معرفی شد.

هدف اصلی DDD:

هدف اصلی DDD ایجاد نرم‌افزاری است که:

  1. به طور دقیق منطق کسب‌وکار را منعکس کند: مدل نرم‌افزاری باید به طور مستقیم با مفاهیم و قوانین کسب‌وکار همسو باشد.
  2. پیچیدگی دامنه را مدیریت کند: DDD ابزارها و الگوهایی را برای مقابله با پیچیدگی‌های دامنه‌های بزرگ و پیچیده ارائه می‌دهد.
  3. ارتباط بین متخصصان دامنه و توسعه‌دهندگان را بهبود بخشد: با ایجاد یک زبان مشترک (Ubiquitous Language)، سوءتفاهم‌ها کاهش یافته و همکاری مؤثرتر می‌شود.
  4. کدی با قابلیت نگهداری و توسعه‌پذیری بالا تولید کند: کدی که مدل دامنه را به خوبی منعکس می‌کند، معمولاً قابل فهم‌تر، قابل نگهداری‌تر و توسعه‌پذیرتر است.

DDD بر این ایده استوار است که قلب نرم‌افزار (The Heart of Software) در مدل دامنه آن نهفته است. بنابراین، باید زمان و تلاش زیادی را صرف درک عمیق دامنه و مدل‌سازی دقیق آن کرد.

سوال 2: Ubiquitous Language (زبان فراگیر) در DDD چیست و چرا اهمیت دارد؟

پاسخ: Ubiquitous Language (زبان فراگیر) یک زبان مشترک و دقیق است که توسط تمام اعضای تیم پروژه، شامل متخصصان دامنه (Domain Experts) و توسعه‌دهندگان (Developers)، برای بحث و مدل‌سازی دامنه کسب‌وکار استفاده می‌شود. این زبان باید در تمام جنبه‌های پروژه، از مکالمات روزمره و مستندات گرفته تا کد منبع، به طور مداوم و بدون ابهام به کار گرفته شود.

اهمیت Ubiquitous Language:

  1. کاهش سوءتفاهم‌ها: با استفاده از یک زبان مشترک، ابهامات و سوءتفاهم‌ها بین متخصصان دامنه و توسعه‌دهندگان به حداقل می‌رسد. هر اصطلاح باید معنای واحد و مشخصی برای همه داشته باشد.
  2. مدل‌سازی دقیق‌تر: زبان فراگیر به تیم کمک می‌کند تا مدل دامنه را با دقت بیشتری تعریف و پالایش کند. اصطلاحات مبهم یا متناقض نشان‌دهنده نقص در درک دامنه هستند.
  3. انعکاس در کد: این زبان مستقیماً در نام‌گذاری کلاس‌ها، متدها، متغیرها و سایر عناصر کد منبع منعکس می‌شود. این باعث می‌شود کد قابل فهم‌تر و قابل نگهداری‌تر باشد، زیرا منطق کسب‌وکار به وضوح در آن دیده می‌شود.
  4. همکاری مؤثرتر: تسهیل ارتباط و همکاری بین اعضای تیم، به ویژه بین متخصصان کسب‌وکار و فنی.
  5. تغییرات آسان‌تر: وقتی مدل دامنه و کد با یک زبان مشترک بیان می‌شوند، تغییرات در الزامات کسب‌وکار به راحتی می‌توانند در کد منعکس شوند.

مثال:

فرض کنید در یک سیستم بانکی، متخصصان دامنه از اصطلاح “حساب مشتری” (Customer Account) استفاده می‌کنند. اگر توسعه‌دهندگان در کد خود از “User Profile” یا “Client Record” استفاده کنند، این باعث سردرگمی و عدم همسویی می‌شود. در یک پروژه DDD، همه باید از “Customer Account” استفاده کنند و این اصطلاح باید در کد نیز به همین شکل (مثلاً کلاس CustomerAccount) ظاهر شود.

Ubiquitous Language یک ابزار زنده است که با تکامل درک تیم از دامنه، تکامل می‌یابد.

سوال 3: Bounded Context (محدوده بافت) در DDD چیست و چرا به آن نیاز داریم؟

پاسخ: Bounded Context (محدوده بافت) یک مفهوم کلیدی در Domain-Driven Design است که به مدیریت پیچیدگی در دامنه‌های بزرگ کمک می‌کند. یک Bounded Context یک مرز منطقی و مفهومی است که در داخل آن، یک مدل دامنه خاص و Ubiquitous Language مربوط به آن، معنای دقیق و بدون ابهام خود را حفظ می‌کند.

چرا به Bounded Context نیاز داریم؟

  1. مدیریت پیچیدگی در دامنه‌های بزرگ: در دامنه‌های بزرگ، یک اصطلاح ممکن است در بخش‌های مختلف کسب‌وکار معانی متفاوتی داشته باشد. تلاش برای ایجاد یک مدل واحد و یکپارچه برای کل دامنه منجر به یک مدل بزرگ، پیچیده و مبهم می‌شود که مدیریت آن دشوار است.
    • مثال: در یک شرکت تجارت الکترونیک، “محصول” (Product) در بخش “کاتالوگ” (Catalog) ممکن است ویژگی‌هایی مانند نام، توضیحات، قیمت و تصویر داشته باشد. اما در بخش “انبارداری” (Warehousing)، “محصول” ممکن است ویژگی‌هایی مانند مکان قفسه، وزن، ابعاد و تاریخ انقضا داشته باشد. این دو “محصول”، با وجود نام یکسان، مفاهیم متفاوتی در بافت‌های مختلف دارند.
  2. حفظ یکپارچگی مدل: هر Bounded Context مدل دامنه خود را دارد که در داخل آن مرزها یکپارچه و سازگار است. این از تداخل و ابهام بین مدل‌های مختلف جلوگیری می‌کند.
  3. استقلال تیم‌ها: Bounded Context ها می‌توانند به تیم‌های مختلف اختصاص داده شوند، که به آن‌ها اجازه می‌دهد تا به صورت مستقل روی مدل دامنه خود کار کنند و مسئولیت‌های واضحی داشته باشند.
  4. پشتیبانی از معماری Microservices: Bounded Context ها به طور طبیعی با معماری Microservices همسو هستند. هر Microservice می‌تواند یک Bounded Context یا مجموعه‌ای از Bounded Context های کوچک‌تر را کپسوله کند.
  5. مدیریت ترجمه بین Context ها: وقتی Bounded Context های مختلف نیاز به تعامل با یکدیگر دارند، DDD الگوهایی (مانند Anti-Corruption Layer, Shared Kernel, Conformist) را برای مدیریت ترجمه بین مدل‌های مختلف ارائه می‌دهد.

ویژگی‌های Bounded Context:

به طور خلاصه، Bounded Context به ما کمک می‌کند تا دامنه‌های بزرگ را به بخش‌های کوچک‌تر و قابل مدیریت‌تر تقسیم کنیم، که هر کدام مدل دامنه و زبان مشترک خاص خود را دارند و این امر به مدیریت پیچیدگی و توسعه مؤثرتر نرم‌افزار کمک می‌کند.

سوال 4: تفاوت بین Core Domain، Supporting Subdomain و Generic Subdomain چیست؟

پاسخ: در Domain-Driven Design (DDD)، یک دامنه بزرگ به زیردامنه‌ها (Subdomains) تقسیم می‌شود. این زیردامنه‌ها بر اساس اهمیت استراتژیک و پیچیدگی کسب‌وکار به سه دسته اصلی تقسیم می‌شوند:

  1. Core Domain (دامنه اصلی):
    • توضیح: Core Domain قلب کسب‌وکار شما و منبع اصلی مزیت رقابتی شماست. این بخشی از سیستم است که کسب‌وکار شما را منحصر به فرد می‌کند و بیشترین ارزش را ایجاد می‌کند. پیچیدگی‌های این بخش باید با دقت و عمق زیادی مدل‌سازی شوند.
    • ویژگی‌ها:
      • منبع مزیت رقابتی: چیزی که کسب‌وکار شما را از رقبا متمایز می‌کند.
      • پیچیدگی بالا: معمولاً شامل پیچیده‌ترین منطق کسب‌وکار است.
      • سرمایه‌گذاری بالا: بیشترین تلاش و بهترین توسعه‌دهندگان باید روی این بخش متمرکز شوند.
      • تغییرات مکرر: این بخش اغلب در حال تکامل و تغییر است.
    • مثال: در یک شرکت تجارت الکترونیک، سیستم “توصیه محصول شخصی‌سازی شده” (Personalized Product Recommendation) یا “الگوریتم قیمت‌گذاری پویا” (Dynamic Pricing Algorithm) می‌تواند Core Domain باشد.
  2. Supporting Subdomain (زیردامنه پشتیبان):
    • توضیح: Supporting Subdomain ها برای عملکرد Core Domain ضروری هستند، اما به خودی خود مزیت رقابتی ایجاد نمی‌کنند. این‌ها معمولاً شامل منطق کسب‌وکار خاصی هستند که برای کسب‌وکار شما منحصر به فرد است، اما پیچیدگی آن‌ها به اندازه Core Domain نیست.
    • ویژگی‌ها:
      • ضروری برای Core Domain: بدون آن‌ها، Core Domain نمی‌تواند به درستی کار کند.
      • منحصر به فرد برای کسب‌وکار: معمولاً نمی‌توان آن‌ها را به صورت آماده از بازار خریداری کرد.
      • پیچیدگی متوسط: منطق کسب‌وکار خاصی دارند اما به اندازه Core Domain پیچیده نیستند.
    • مثال: در همان شرکت تجارت الکترونیک، سیستم “مدیریت سفارش” (Order Management) یا “مدیریت موجودی” (Inventory Management) می‌تواند Supporting Subdomain باشد. این‌ها برای کسب‌وکار ضروری هستند، اما ممکن است الگوریتم‌های پیچیده و منحصر به فردی نداشته باشند که مزیت رقابتی ایجاد کنند.
  3. Generic Subdomain (زیردامنه عمومی):
    • توضیح: Generic Subdomain ها بخش‌هایی از سیستم هستند که برای هر کسب‌وکاری عمومی و استاندارد محسوب می‌شوند و مزیت رقابتی ایجاد نمی‌کنند. این‌ها می‌توانند به صورت آماده (Off-the-shelf) خریداری شوند یا با حداقل تلاش توسعه یابند.
    • ویژگی‌ها:
      • عمومی و استاندارد: منطق کسب‌وکار آن‌ها برای بسیاری از کسب‌وکارها مشترک است.
      • بدون مزیت رقابتی: پیاده‌سازی آن‌ها به کسب‌وکار شما برتری نمی‌دهد.
      • قابل خرید/استفاده مجدد: می‌توانند به صورت پکیج‌های آماده، کتابخانه‌ها یا سرویس‌های شخص ثالث استفاده شوند.
    • مثال: سیستم “احراز هویت و مدیریت کاربران” (Authentication and User Management)، “سیستم لاگینگ” (Logging System) یا “سیستم پرداخت عمومی” (Generic Payment Gateway Integration) می‌توانند Generic Subdomain باشند.

اهمیت تفکیک زیردامنه‌ها:

تفکیک زیردامنه‌ها به تیم‌ها کمک می‌کند تا منابع و تلاش خود را به طور مؤثرتری متمرکز کنند. بیشترین سرمایه‌گذاری باید روی Core Domain انجام شود، در حالی که برای Generic Subdomain ها می‌توان از راه‌حل‌های آماده استفاده کرد تا زمان و هزینه توسعه کاهش یابد.

سوال 5: Aggregate در DDD چیست و چرا از آن استفاده می‌کنیم؟

پاسخ: Aggregate (تجمیع) یک مفهوم حیاتی در Domain-Driven Design است که به مدیریت یکپارچگی داده‌ها و حفظ consistency (سازگاری) در مدل دامنه کمک می‌کند. یک Aggregate مجموعه‌ای از Entity ها و Value Object ها است که به عنوان یک واحد منطقی در نظر گرفته می‌شوند و توسط یک Entity خاص به نام Aggregate Root (ریشه تجمیع) محافظت می‌شوند.

ویژگی‌های اصلی Aggregate:

  1. Aggregate Root: هر Aggregate یک و تنها یک Aggregate Root دارد. Aggregate Root تنها Entity ای است که می‌توان از خارج Aggregate به آن ارجاع داد. تمام عملیات بر روی Aggregate باید از طریق Aggregate Root انجام شود.
  2. مرز Consistency: Aggregate یک مرز Consistency (سازگاری) را تعریف می‌کند. این بدان معناست که تمام invariants (قوانین کسب‌وکار که همیشه باید درست باشند) در داخل Aggregate باید در پایان هر عملیات (تراکنش) حفظ شوند.
  3. حفاظت از Invariants: Aggregate Root مسئولیت حفظ invariants تمام Entity ها و Value Object های داخل Aggregate را بر عهده دارد. هیچ شیئی خارج از Aggregate نباید به طور مستقیم به Entity های داخلی Aggregate دسترسی داشته باشد.
  4. حذف آبشاری (Cascading Delete): وقتی Aggregate Root حذف می‌شود، تمام Entity ها و Value Object های داخل آن نیز باید حذف شوند.

چرا از Aggregate استفاده می‌کنیم؟

  1. حفظ Consistency و یکپارچگی داده‌ها: مهم‌ترین دلیل استفاده از Aggregate، تضمین سازگاری مدل دامنه است. با کپسوله‌سازی منطق و داده‌ها در یک Aggregate و اعمال قوانین کسب‌وکار از طریق Aggregate Root، از وضعیت‌های نامعتبر جلوگیری می‌شود.
    • مثال: در یک Aggregate Order، Order می‌تواند Aggregate Root باشد و OrderLineItem ها Entity های داخلی باشند. وقتی یک OrderLineItem اضافه یا حذف می‌شود، Order (Aggregate Root) مسئولیت به‌روزرسانی قیمت کل سفارش و اطمینان از اینکه موجودی محصول کافی است را بر عهده دارد.
  2. مدیریت تراکنش‌ها: Aggregate ها معمولاً به عنوان واحد اتمی برای تراکنش‌های دیتابیس در نظر گرفته می‌شوند. یک تراکنش باید تمام تغییرات را در یک Aggregate به صورت اتمی (همه یا هیچ) اعمال کند.
  3. کاهش کوپلینگ: با محدود کردن دسترسی به Entity های داخلی از طریق Aggregate Root، کوپلینگ بین Aggregate ها کاهش می‌یابد. این باعث می‌شود تغییرات در یک Aggregate کمتر بر سایر Aggregate ها تأثیر بگذارد.
  4. ساده‌سازی مدل‌سازی: Aggregate ها به ما کمک می‌کنند تا مدل دامنه را به بخش‌های کوچک‌تر و قابل مدیریت‌تر تقسیم کنیم، که درک و پیاده‌سازی آن‌ها آسان‌تر است.

مثال:

public class Order : AggregateRoot // Order به عنوان Aggregate Root
{
    public Guid Id { get; private set; }
    public int CustomerId { get; private set; }
    public DateTime OrderDate { get; private set; }
    public OrderStatus Status { get; private set; }
    private readonly List<OrderLineItem> _lineItems; // OrderLineItem ها Entity های داخلی
    public IReadOnlyList<OrderLineItem> LineItems => _lineItems.AsReadOnly();

    // سازنده
    private Order() { _lineItems = new List<OrderLineItem>(); }

    public static Order Create(Guid id, int customerId)
    {
        var order = new Order { Id = id, CustomerId = customerId, OrderDate = DateTime.UtcNow, Status = OrderStatus.Pending };
        // افزودن رویداد دامنه
        return order;
    }

    public void AddLineItem(Guid productId, int quantity, decimal unitPrice)
    {
        // منطق کسب‌وکار و حفظ invariants
        if (quantity <= 0) throw new ArgumentException("Quantity must be positive.");
        var lineItem = new OrderLineItem(Guid.NewGuid(), productId, quantity, unitPrice);
        _lineItems.Add(lineItem);
        // افزودن رویداد دامنه
    }

    public void ConfirmOrder()
    {
        if (Status != OrderStatus.Pending) throw new InvalidOperationException("Order cannot be confirmed.");
        Status = OrderStatus.Confirmed;
        // افزودن رویداد دامنه
    }

    // ... سایر متدهای عملیاتی که invariants را حفظ می‌کنند
}

public class OrderLineItem : Entity // OrderLineItem یک Entity داخلی است
{
    public Guid Id { get; private set; }
    public Guid ProductId { get; private set; }
    public int Quantity { get; private set; }
    public decimal UnitPrice { get; private set; }

    public OrderLineItem(Guid id, Guid productId, int quantity, decimal unitPrice)
    {
        Id = id;
        ProductId = productId;
        Quantity = quantity;
        UnitPrice = unitPrice;
    }
}

public enum OrderStatus { Pending, Confirmed, Shipped, Cancelled }

در این مثال، Order Aggregate Root است. تمام عملیات بر روی OrderLineItem ها باید از طریق متدهای Order (مانند AddLineItem) انجام شود تا Order بتواند قوانین کسب‌وکار مربوط به سفارش را حفظ کند.

سوال 6: Entity در DDD چیست و چه ویژگی‌هایی دارد؟

پاسخ: Entity (موجودیت) یک مفهوم اساسی در Domain-Driven Design (DDD) است که یک شیء در مدل دامنه را نشان می‌دهد که دارای یک هویت منحصر به فرد (Unique Identity) است و این هویت در طول زمان و تغییرات حالت آن شیء، ثابت باقی می‌ماند. Entity ها معمولاً شامل منطق کسب‌وکار (Behavior) و داده‌ها (State) هستند.

ویژگی‌های اصلی Entity:

  1. هویت منحصر به فرد (Unique Identity):
    • مهم‌ترین ویژگی یک Entity، هویت آن است. دو Entity با وجود اینکه ممکن است تمام ویژگی‌هایشان یکسان باشد، اگر هویت‌های متفاوتی داشته باشند، دو شیء مجزا محسوب می‌شوند.
    • این هویت معمولاً توسط یک شناسه (ID) مانند Guid یا int (در دیتابیس) نمایش داده می‌شود.
    • مثال: دو مشتری ممکن است نام و آدرس یکسانی داشته باشند، اما اگر ID های متفاوتی داشته باشند، دو مشتری مجزا هستند.
  2. چرخه حیات (Lifecycle):
    • Entity ها دارای یک چرخه حیات هستند که شامل ایجاد، تغییر حالت و حذف می‌شود. هویت آن‌ها در طول این چرخه حیات ثابت می‌ماند.
  3. رفتار (Behavior):
    • Entity ها علاوه بر داده‌ها، شامل منطق کسب‌وکار (متدها) نیز هستند که عملیات مربوط به آن موجودیت را انجام می‌دهند و invariants (قوانین کسب‌وکار) را حفظ می‌کنند.
  4. تغییرپذیری (Mutability):
    • حالت Entity ها می‌تواند در طول زمان تغییر کند.

چرا از Entity استفاده می‌کنیم؟

  1. مدل‌سازی اشیاء با هویت: برای مدل‌سازی مفاهیمی که نیاز به ردیابی و تمایز بر اساس هویت دارند، حتی اگر ویژگی‌هایشان تغییر کند.
  2. حفظ منطق کسب‌وکار: کپسوله‌سازی داده‌ها و رفتار مربوط به یک موجودیت در یک مکان واحد.

مثال:

public class Customer
{
    public Guid Id { get; private set; } // هویت منحصر به فرد
    public string Name { get; private set; }
    public string Email { get; private set; }

    public Customer(Guid id, string name, string email)
    {
        Id = id;
        Name = name;
        Email = email;
    }

    public void ChangeEmail(string newEmail)
    {
        // منطق کسب‌وکار برای تغییر ایمیل
        if (string.IsNullOrWhiteSpace(newEmail)) throw new ArgumentException("Email cannot be empty.");
        Email = newEmail;
    }
}

در این مثال، Customer یک Entity است. حتی اگر نام و ایمیل مشتری تغییر کند، Id آن ثابت می‌ماند و مشتری را به صورت منحصر به فرد شناسایی می‌کند.

سوال 7: Value Object در DDD چیست و چه ویژگی‌هایی دارد؟

پاسخ: Value Object (شیء ارزشی) یک مفهوم اساسی دیگر در Domain-Driven Design (DDD) است که یک شیء در مدل دامنه را نشان می‌دهد که فاقد هویت منحصر به فرد است و تنها با ویژگی‌هایش تعریف می‌شود. Value Object ها معمولاً برای مدل‌سازی مفاهیمی استفاده می‌شوند که ماهیت آن‌ها با مقادیرشان تعیین می‌شود، نه با یک شناسه خاص.

ویژگی‌های اصلی Value Object:

  1. فاقد هویت (No Identity):
    • مهم‌ترین ویژگی Value Object این است که هویت منحصر به فردی ندارد. دو Value Object با ویژگی‌های یکسان، برابر (equal) محسوب می‌شوند.
    • مثال: دو شیء Money که هر دو مقدار 100 دلار را نشان می‌دهند، برابر هستند، حتی اگر در دو مکان مختلف از حافظه ایجاد شده باشند.
  2. تغییرناپذیری (Immutability):
    • Value Object ها باید تغییرناپذیر باشند. به این معنی که پس از ایجاد، حالت آن‌ها قابل تغییر نیست. اگر نیاز به تغییر مقداری باشد، یک Value Object جدید با مقادیر جدید ایجاد می‌شود.
    • این ویژگی باعث می‌شود Value Object ها thread-safe باشند و اشتراک‌گذاری آن‌ها بدون نگرانی از تغییرات ناخواسته امکان‌پذیر باشد.
  3. برابری بر اساس مقدار (Equality by Value):
    • برابری Value Object ها بر اساس مقایسه تمام ویژگی‌هایشان تعیین می‌شود، نه بر اساس مرجع (reference).
  4. کپسوله‌سازی (Encapsulation):
    • Value Object ها می‌توانند شامل منطق کسب‌وکار مربوط به مقادیر خود باشند.

چرا از Value Object استفاده می‌کنیم؟

  1. افزایش خوانایی و بیان‌پذیری مدل: با کپسوله‌سازی مقادیر مرتبط در یک شیء با معنی دامنه، مدل خواناتر و بیان‌پذیرتر می‌شود. به جای ارسال چندین پارامتر، یک Value Object ارسال می‌شود.
  2. کاهش خطا: با اعمال invariants (قوانین کسب‌وکار) در سازنده Value Object، از ایجاد اشیاء با حالت نامعتبر جلوگیری می‌شود.
  3. کاهش پیچیدگی: با حذف هویت، مدیریت Value Object ها ساده‌تر می‌شود. نیازی به ردیابی آن‌ها در دیتابیس یا مدیریت چرخه حیات پیچیده نیست.
  4. افزایش ایمنی: تغییرناپذیری باعث می‌شود Value Object ها در محیط‌های چند رشته‌ای ایمن باشند.

مثال:

public class Money : ValueObject
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0) throw new ArgumentException("Amount cannot be negative.");
        if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Currency cannot be empty.");
        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (Currency != other.Currency) throw new InvalidOperationException("Cannot add money of different currencies.");
        return new Money(Amount + other.Amount, Currency);
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}

در این مثال، Money یک Value Object است. دو شیء Money با Amount و Currency یکسان، برابر محسوب می‌شوند. متد Add یک Money جدید را برمی‌گرداند، زیرا Money تغییرناپذیر است.

سوال 8: تفاوت اصلی بین Entity و Value Object چیست؟

پاسخ: تفاوت اصلی بین Entity و Value Object در Domain-Driven Design (DDD) در مفهوم هویت (Identity) و تغییرپذیری (Mutability) آن‌ها نهفته است. درک این تفاوت برای مدل‌سازی صحیح دامنه بسیار حیاتی است.

ویژگی Entity Value Object
هویت دارای هویت منحصر به فرد (Unique Identity) است که در طول زمان ثابت می‌ماند. فاقد هویت منحصر به فرد است؛ با مقادیرش تعریف می‌شود.
برابری برابری بر اساس هویت (مرجع) تعیین می‌شود. برابری بر اساس مقادیر تمام ویژگی‌ها تعیین می‌شود.
تغییرپذیری تغییرپذیر (Mutable) است؛ حالت آن می‌تواند در طول زمان تغییر کند. تغییرناپذیر (Immutable) است؛ پس از ایجاد، حالت آن قابل تغییر نیست.
چرخه حیات دارای چرخه حیات (ایجاد، تغییر، حذف) است. فاقد چرخه حیات مستقل؛ وجود آن به Entity ای که آن را شامل می‌شود، وابسته است.
هدف مدل‌سازی اشیائی که نیاز به ردیابی و تمایز بر اساس هویت دارند. مدل‌سازی مفاهیمی که ماهیت آن‌ها با مقادیرشان تعیین می‌شود و برای توصیف ویژگی‌های Entity ها استفاده می‌شوند.
مثال Customer, Order, Product (در بافت کاتالوگ) Address, Money, DateRange, Color

خلاصه:

اگر شیء شما نیاز به ردیابی منحصر به فرد در طول زمان دارد و حالت آن ممکن است تغییر کند، آن یک Entity است. اگر شیء شما تنها با مقادیرش تعریف می‌شود و پس از ایجاد تغییر نمی‌کند، آن یک Value Object است. استفاده صحیح از این دو مفهوم به ایجاد یک مدل دامنه قوی‌تر، خواناتر و قابل نگهداری‌تر کمک می‌کند.

سوال 9: Domain Service در DDD چیست و چه زمانی از آن استفاده می‌کنیم؟

پاسخ: Domain Service (سرویس دامنه) یک مفهوم در Domain-Driven Design (DDD) است که برای کپسوله‌سازی منطق کسب‌وکاری استفاده می‌شود که به طور طبیعی به یک Entity یا Value Object خاص تعلق ندارد. این منطق معمولاً شامل هماهنگی بین چندین Aggregate یا انجام عملیاتی است که ماهیت فرایندی دارند و نه ماهیت موجودیتی.

ویژگی‌های اصلی Domain Service:

  1. فاقد حالت (Stateless): Domain Service ها معمولاً stateless هستند؛ یعنی هیچ حالت داخلی خاصی را در طول زمان نگهداری نمی‌کنند. آن‌ها فقط عملیات را انجام می‌دهند.
  2. نام‌گذاری بر اساس فعالیت (Named after an activity): نام Domain Service ها باید نشان‌دهنده یک فعالیت یا فرآیند کسب‌وکار باشد (مثلاً OrderPlacementService، TransferMoneyService).
  3. هماهنگ‌کننده (Coordinator): Domain Service ها می‌توانند چندین Aggregate را برای انجام یک عملیات پیچیده هماهنگ کنند.
  4. مربوط به دامنه (Domain-related): منطق موجود در Domain Service باید کاملاً مربوط به دامنه کسب‌وکار باشد و نه به زیرساخت یا لایه کاربرد.

چه زمانی از Domain Service استفاده می‌کنیم؟

از Domain Service زمانی استفاده می‌کنیم که:

  1. منطق کسب‌وکار به یک Entity یا Value Object خاص تعلق ندارد: اگر یک عملیات شامل چندین Entity یا Value Object باشد و منطق آن به طور طبیعی در هیچ یک از آن‌ها قرار نگیرد.
  2. نیاز به هماهنگی بین چندین Aggregate باشد: برای مثال، انتقال پول بین دو حساب بانکی (که هر حساب یک Aggregate است) نیاز به هماهنگی دارد که در یک Domain Service قرار می‌گیرد.
  3. عملیات ماهیت فرایندی داشته باشد: عملیاتی که یک فرآیند کسب‌وکار را نشان می‌دهد و نه یک ویژگی یا رفتار یک شیء خاص.

مثال:

فرض کنید در یک سیستم بانکی، نیاز به انتقال وجه از یک حساب به حساب دیگر داریم. این عملیات شامل دو Aggregate مجزا (SourceAccount و DestinationAccount) است. منطق انتقال وجه به طور طبیعی به هیچ یک از این حساب‌ها تعلق ندارد، بلکه یک فرآیند بین آن‌هاست. بنابراین، می‌توانیم یک Domain Service به نام TransferMoneyService ایجاد کنیم:

public class TransferMoneyService
{
    private readonly IAccountRepository _accountRepository;

    public TransferMoneyService(IAccountRepository accountRepository)
    {
        _accountRepository = accountRepository;
    }

    public void Transfer(Guid sourceAccountId, Guid destinationAccountId, decimal amount)
    {
        // 1. دریافت حساب‌ها از Repository
        var sourceAccount = _accountRepository.GetById(sourceAccountId);
        var destinationAccount = _accountRepository.GetById(destinationAccountId);

        if (sourceAccount == null || destinationAccount == null)
        {
            throw new ArgumentException("One or both accounts not found.");
        }

        // 2. اعمال منطق کسب‌وکار (برداشت و واریز)
        sourceAccount.Withdraw(amount);
        destinationAccount.Deposit(amount);

        // 3. ذخیره تغییرات (معمولاً در لایه Application Service یا Unit of Work مدیریت می‌شود)
        _accountRepository.Save(sourceAccount);
        _accountRepository.Save(destinationAccount);

        // افزودن رویداد دامنه (مثلاً MoneyTransferredEvent)
    }
}

در این مثال، TransferMoneyService مسئول هماهنگی عملیات برداشت و واریز بین دو حساب است و منطق مربوط به این فرآیند را کپسوله می‌کند.

سوال 10: Application Service در DDD چیست و چه زمانی از آن استفاده می‌کنیم؟

پاسخ: Application Service (سرویس کاربرد) در Domain-Driven Design (DDD) یک لایه نازک است که وظیفه هماهنگی عملیات بین لایه رابط کاربری (UI) و لایه دامنه را بر عهده دارد. Application Service ها منطق کسب‌وکار را شامل نمی‌شوند، بلکه جریان کار (workflow) را مدیریت می‌کنند و مسئولیت‌های زیرساختی مانند مدیریت تراکنش‌ها، امنیت و ارسال رویدادها را بر عهده دارند.

ویژگی‌های اصلی Application Service:

  1. هماهنگ‌کننده (Coordinator): Application Service ها درخواست‌های ورودی را از لایه رابط کاربری دریافت می‌کنند و آن‌ها را به عملیات مناسب در لایه دامنه (Entities, Aggregates, Domain Services) نگاشت می‌کنند.
  2. فاقد منطق کسب‌وکار (No Business Logic): Application Service ها نباید شامل منطق کسب‌وکار باشند. تمام منطق کسب‌وکار باید در لایه دامنه قرار گیرد.
  3. مدیریت تراکنش‌ها (Transaction Management): Application Service ها معمولاً مسئول شروع و پایان تراکنش‌های دیتابیس هستند.
  4. مدیریت امنیت (Security Management): می‌توانند مسئول اعمال قوانین امنیتی (مانند احراز هویت و مجوز) باشند.
  5. ارسال رویدادها (Event Dispatching): پس از انجام عملیات، می‌توانند رویدادهای دامنه را منتشر کنند.
  6. نام‌گذاری بر اساس مورد استفاده (Named after a use case): نام Application Service ها باید نشان‌دهنده یک مورد استفاده یا یک فرمان (command) باشد (مثلاً OrderService، ProductManagementService، RegisterUserService).

چه زمانی از Application Service استفاده می‌کنیم؟

از Application Service زمانی استفاده می‌کنیم که:

  1. نیاز به هماهنگی بین لایه رابط کاربری و لایه دامنه باشد: برای دریافت درخواست‌ها از UI و فراخوانی متدهای مناسب در لایه دامنه.
  2. نیاز به مدیریت تراکنش‌ها باشد: برای اطمینان از اتمی بودن عملیات.
  3. نیاز به اعمال قوانین امنیتی باشد: قبل از فراخوانی منطق دامنه.
  4. نیاز به انتشار رویدادهای دامنه باشد: پس از انجام موفقیت‌آمیز یک عملیات.

مثال:

public class OrderApplicationService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IDomainEventDispatcher _eventDispatcher;

    public OrderApplicationService(IOrderRepository orderRepository, IProductRepository productRepository, IDomainEventDispatcher eventDispatcher)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _eventDispatcher = eventDispatcher;
    }

    // یک Command Handler برای ایجاد سفارش جدید
    public void CreateOrder(CreateOrderCommand command)
    {
        // 1. اعتبارسنجی ورودی (اختیاری، می‌تواند در لایه بالاتر هم انجام شود)
        if (command.Quantity <= 0) throw new ArgumentException("Quantity must be positive.");

        // 2. دریافت Aggregate Root از Repository
        var product = _productRepository.GetById(command.ProductId);
        if (product == null) throw new ArgumentException("Product not found.");

        // 3. فراخوانی منطق دامنه
        var order = Order.Create(Guid.NewGuid(), command.CustomerId);
        order.AddLineItem(product.Id, command.Quantity, product.Price);

        // 4. ذخیره Aggregate Root
        _orderRepository.Add(order);

        // 5. انتشار رویدادهای دامنه (مثلاً OrderCreatedEvent)
        _eventDispatcher.Dispatch(order.DomainEvents);
    }

    // یک Command Handler برای تایید سفارش
    public void ConfirmOrder(Guid orderId)
    {
        var order = _orderRepository.GetById(orderId);
        if (order == null) throw new ArgumentException("Order not found.");

        order.ConfirmOrder(); // فراخوانی منطق دامنه

        _orderRepository.Save(order);
        _eventDispatcher.Dispatch(order.DomainEvents);
    }
}

در این مثال، OrderApplicationService درخواست‌های CreateOrderCommand و ConfirmOrderCommand را دریافت می‌کند، Aggregate Order را از Repository بازیابی می‌کند، متدهای دامنه را فراخوانی می‌کند و سپس تغییرات را ذخیره کرده و رویدادها را منتشر می‌کند. این سرویس هیچ منطق کسب‌وکار مستقیمی ندارد.

سوال 11: Repository در DDD چیست و چه نقشی دارد؟

پاسخ: Repository (مخزن) یک مفهوم طراحی در Domain-Driven Design (DDD) است که به عنوان یک واسط بین لایه دامنه و لایه زیرساخت (مانند دیتابیس) عمل می‌کند. Repository ها مسئولیت بازیابی و ذخیره‌سازی Aggregate Root ها را بر عهده دارند.

نقش Repository:

  1. کپسوله‌سازی منطق دسترسی به داده: Repository ها تمام جزئیات مربوط به نحوه ذخیره‌سازی و بازیابی اشیاء دامنه را از لایه دامنه پنهان می‌کنند. لایه دامنه فقط با Repository ها کار می‌کند و نیازی به دانستن جزئیات دیتابیس، ORM یا سایر مکانیزم‌های ذخیره‌سازی ندارد.
  2. مدیریت چرخه حیات Aggregate Root ها: Repository ها مسئولیت ایجاد، بازیابی، به‌روزرسانی و حذف Aggregate Root ها را بر عهده دارند.
  3. ارائه مجموعه‌ای از اشیاء: Repository ها به گونه‌ای طراحی می‌شوند که گویی یک مجموعه (Collection) از اشیاء دامنه را در حافظه نگهداری می‌کنند. این باعث می‌شود که لایه دامنه بتواند با اشیاء خود به صورت مجموعه‌ای از اشیاء در حافظه کار کند، بدون اینکه نگران جزئیات ذخیره‌سازی باشد.
  4. فقط با Aggregate Root ها کار می‌کند: Repository ها همیشه با Aggregate Root ها کار می‌کنند، نه با Entity های داخلی Aggregate. این به حفظ مرزهای Aggregate و consistency آن کمک می‌کند.

ویژگی‌های Repository:

مثال:

public interface IOrderRepository
{
    Order GetById(Guid id);
    void Add(Order order);
    void Update(Order order);
    void Remove(Order order);
    IEnumerable<Order> GetOrdersByCustomerId(int customerId);
}

این واسط در لایه دامنه تعریف می‌شود و پیاده‌سازی آن (مثلاً EfCoreOrderRepository) در لایه زیرساخت قرار می‌گیرد.

سوال 12: Domain Event در DDD چیست و چه زمانی از آن استفاده می‌کنیم؟

پاسخ: Domain Event (رویداد دامنه) چیزی است که در دامنه کسب‌وکار اتفاق افتاده و سایر بخش‌های سیستم یا سیستم‌های خارجی ممکن است به آن علاقه‌مند باشند. Domain Event ها به ما کمک می‌کنند تا کوپلینگ (coupling) بین بخش‌های مختلف دامنه را کاهش دهیم و سیستم‌های مقیاس‌پذیرتر و قابل نگهداری‌تری بسازیم.

ویژگی‌های اصلی Domain Event:

  1. بیانگر اتفاقی در گذشته: Domain Event ها همیشه بیانگر چیزی هستند که در گذشته اتفاق افتاده است (مثلاً OrderCreated، ProductStockReduced).
  2. تغییرناپذیر (Immutable): پس از ایجاد، Domain Event ها نباید تغییر کنند.
  3. نام‌گذاری با پسوند Event: نام آن‌ها معمولاً با پسوند Event یا ed (گذشته) همراه است.
  4. شامل داده‌های مربوط به رویداد: Domain Event ها باید شامل تمام داده‌های لازم برای پردازش رویداد توسط شنوندگان باشند.

چه زمانی از Domain Event استفاده می‌کنیم؟

از Domain Event زمانی استفاده می‌کنیم که:

  1. نیاز به انتشار تغییرات در دامنه باشد: وقتی یک عملیات در یک Aggregate منجر به تغییراتی می‌شود که سایر Aggregate ها یا بخش‌های سیستم باید از آن مطلع شوند.
  2. نیاز به کاهش کوپلینگ باشد: به جای اینکه یک Aggregate به طور مستقیم Aggregate دیگری را فراخوانی کند، می‌تواند یک Domain Event منتشر کند.
  3. نیاز به اجرای عملیات جانبی (side effects) باشد: عملیاتی که بخشی از منطق اصلی کسب‌وکار نیستند اما باید پس از یک رویداد خاص اجرا شوند (مثلاً ارسال ایمیل، به‌روزرسانی گزارش‌ها).
  4. نیاز به Event Sourcing باشد: در معماری Event Sourcing، تمام تغییرات حالت سیستم به صورت دنباله‌ای از Domain Event ها ذخیره می‌شوند.

مثال:

public class OrderCreatedEvent : IDomainEvent
{
    public Guid OrderId { get; }
    public int CustomerId { get; }
    public DateTime OrderDate { get; }

    public OrderCreatedEvent(Guid orderId, int customerId, DateTime orderDate)
    {
        OrderId = orderId;
        CustomerId = customerId;
        OrderDate = orderDate;
    }
}

// در Aggregate Root Order
public class Order : AggregateRoot
{
    // ...
    public static Order Create(Guid id, int customerId)
    {
        var order = new Order { Id = id, CustomerId = customerId, OrderDate = DateTime.UtcNow, Status = OrderStatus.Pending };
        order.AddDomainEvent(new OrderCreatedEvent(order.Id, order.CustomerId, order.OrderDate));
        return order;
    }
    // ...
}

پس از انتشار OrderCreatedEvent، سرویس‌های دیگر می‌توانند به آن گوش دهند و عملیات مربوطه را انجام دهند (مثلاً سرویس پرداخت می‌تواند پرداخت را آغاز کند، سرویس انبارداری می‌تواند موجودی را کاهش دهد).

سوال 13: Domain Service و Application Service چه تفاوت‌هایی دارند؟

پاسخ: Domain Service و Application Service هر دو نوعی از سرویس‌ها در Domain-Driven Design (DDD) هستند، اما نقش‌ها و مسئولیت‌های کاملاً متفاوتی دارند. درک این تفاوت برای طراحی صحیح معماری سیستم بسیار مهم است.

ویژگی Domain Service Application Service
مکان در معماری لایه دامنه (Domain Layer) لایه کاربرد (Application Layer)
هدف اصلی کپسوله‌سازی منطق کسب‌وکار که به یک Entity یا Value Object خاص تعلق ندارد. هماهنگی جریان کار (workflow) بین UI و لایه دامنه؛ مدیریت تراکنش‌ها و امنیت.
شامل منطق کسب‌وکار؟ بله، شامل منطق کسب‌وکار است. خیر، نباید شامل منطق کسب‌وکار باشد.
حالت (State) معمولاً Stateless (فاقد حالت) Stateless
نام‌گذاری بر اساس یک فعالیت یا فرآیند دامنه (مثلاً TransferMoneyService) بر اساس یک مورد استفاده یا فرمان (مثلاً OrderService, CreateOrderCommand)
وابستگی‌ها وابسته به Entities, Value Objects, Repositories وابسته به Domain Services, Repositories
مثال CurrencyConverter, FraudDetectionService ProductManagementService, UserRegistrationService

خلاصه:

Domain Service ها منطق کسب‌وکار را اجرا می‌کنند، در حالی که Application Service ها جریان کار را هماهنگ می‌کنند. Domain Service ها بخشی از مدل دامنه هستند و قوانین کسب‌وکار را اعمال می‌کنند، در حالی که Application Service ها یک لایه نازک بین رابط کاربری و دامنه هستند که مسئولیت‌های فنی و هماهنگی را بر عهده دارند.

سوال 14: Factory در DDD چیست و چه زمانی از آن استفاده می‌کنیم؟

پاسخ: Factory (کارخانه) در Domain-Driven Design (DDD) یک الگوی طراحی است که مسئولیت ایجاد اشیاء پیچیده دامنه، به ویژه Aggregate Root ها و Entity ها را بر عهده دارد. هدف اصلی Factory کپسوله‌سازی منطق پیچیده ساخت اشیاء و اطمینان از ایجاد اشیاء در یک حالت معتبر است.

چرا از Factory استفاده می‌کنیم؟

  1. کپسوله‌سازی منطق ساخت: وقتی فرآیند ساخت یک شیء دامنه پیچیده است (مثلاً نیاز به اعتبارسنجی‌های متعدد، ایجاد چندین شیء مرتبط، یا بازیابی داده از منابع خارجی)، Factory این منطق را کپسوله می‌کند و از پراکنده شدن آن در کد جلوگیری می‌کند.
  2. اطمینان از Invariants: Factory تضمین می‌کند که شیء ایجاد شده در یک حالت معتبر (valid state) قرار دارد و تمام invariants آن رعایت شده‌اند.
  3. جداسازی مسئولیت‌ها: جداسازی مسئولیت ساخت اشیاء از مسئولیت‌های خود اشیاء دامنه. Entity ها و Aggregate Root ها می‌توانند بر منطق کسب‌وکار خود تمرکز کنند.
  4. پنهان کردن جزئیات پیاده‌سازی: Factory می‌تواند جزئیات مربوط به نحوه ایجاد اشیاء را پنهان کند.

انواع Factory:

چه زمانی از Factory استفاده نمی‌کنیم؟

اگر ساخت یک شیء دامنه ساده است و نیازی به منطق پیچیده یا اعتبارسنجی خاصی ندارد، استفاده از سازنده (constructor) مستقیم کافی است و نیازی به Factory نیست.

مثال (متد استاتیک در Aggregate Root):

public class Order : AggregateRoot
{
    // ... (سایر ویژگی‌ها و متدها)

    public static Order Create(Guid id, int customerId, IEnumerable<ProductItem> products)
    {
        if (id == Guid.Empty) throw new ArgumentException("Order ID cannot be empty.");
        if (customerId <= 0) throw new ArgumentException("Customer ID must be positive.");
        if (products == null || !products.Any()) throw new ArgumentException("Order must have at least one product.");

        var order = new Order
        {
            Id = id,
            CustomerId = customerId,
            OrderDate = DateTime.UtcNow,
            Status = OrderStatus.Pending
        };

        foreach (var productItem in products)
        {
            order.AddLineItem(productItem.ProductId, productItem.Quantity, productItem.UnitPrice);
        }

        order.AddDomainEvent(new OrderCreatedEvent(order.Id, order.CustomerId, order.OrderDate));
        return order;
    }
}

در این مثال، متد استاتیک Create در کلاس Order به عنوان یک Factory عمل می‌کند. این متد مسئولیت اعتبارسنجی ورودی‌ها و ایجاد یک شیء Order در یک حالت معتبر را بر عهده دارد.

سوال 15: Bounded Context و Subdomain چه تفاوت‌هایی دارند؟

پاسخ: Bounded Context و Subdomain مفاهیم مرتبط اما متفاوتی در Domain-Driven Design (DDD) هستند. درک تفاوت آن‌ها برای طراحی معماری‌های مقیاس‌پذیر و قابل نگهداری ضروری است.

ویژگی Subdomain (زیردامنه) Bounded Context (محدوده بافت)
ماهیت یک بخش منطقی از دامنه کسب‌وکار. یک مرز صریح در نرم‌افزار که در آن یک مدل دامنه خاص و Ubiquitous Language آن معنای دقیق و بدون ابهام خود را حفظ می‌کند.
مکان در فضای مسئله (Problem Space)؛ مربوط به کسب‌وکار. در فضای راه‌حل (Solution Space)؛ مربوط به طراحی نرم‌افزار.
هدف تقسیم دامنه بزرگ به بخش‌های قابل مدیریت بر اساس منطق کسب‌وکار. مدیریت پیچیدگی مدل‌سازی و توسعه نرم‌افزار با ایجاد مرزهای واضح برای مدل‌های دامنه.
زبان هر Subdomain می‌تواند یک Ubiquitous Language خاص خود را داشته باشد. هر Bounded Context یک Ubiquitous Language خاص خود را دارد که در داخل آن مرزها معتبر است.
ارتباط یک Subdomain می‌تواند به یک یا چند Bounded Context نگاشت شود. یک Bounded Context معمولاً به یک Subdomain خاص نگاشت می‌شود، اما می‌تواند چندین Subdomain را نیز پوشش دهد یا بخشی از یک Subdomain باشد.
مثال Order Management, Customer Support, Billing OrderContext, CustomerContext, BillingContext

خلاصه:

Subdomain ها بخش‌های مفهومی از دامنه کسب‌وکار هستند، در حالی که Bounded Context ها مرزهای پیاده‌سازی نرم‌افزاری هستند که مدل‌های دامنه را در داخل خود کپسوله می‌کنند. یک Bounded Context یک راه‌حل نرم‌افزاری برای یک Subdomain خاص است. این تفکیک به ما کمک می‌کند تا پیچیدگی‌های دامنه را به صورت مؤثرتری مدیریت کنیم و از تداخل مفاهیم در بخش‌های مختلف سیستم جلوگیری کنیم.

سوال 16: CQRS (Command Query Responsibility Segregation) در DDD چیست و چه مزایایی دارد؟

پاسخ: CQRS (Command Query Responsibility Segregation) یک الگوی معماری است که پیشنهاد می‌کند عملیات خواندن (Queries) و نوشتن (Commands) در یک سیستم را از یکدیگر جدا کنیم. این جداسازی به ما اجازه می‌دهد تا مدل‌های داده و فناوری‌های بهینه‌سازی شده متفاوتی را برای هر یک از این عملیات‌ها استفاده کنیم، که منجر به سیستم‌های مقیاس‌پذیرتر، انعطاف‌پذیرتر و با عملکرد بهتر می‌شود.

اجزای اصلی CQRS:

  1. Commands (فرمان‌ها):
    • عملیات‌هایی که حالت سیستم را تغییر می‌دهند (نوشتن).
    • معمولاً شامل یک Command Handler است که Command را دریافت کرده و منطق دامنه را برای تغییر حالت Aggregate ها اجرا می‌کند.
    • مثال: CreateOrderCommand, UpdateProductCommand.
  2. Queries (پرس‌وجوها):
    • عملیات‌هایی که حالت سیستم را بدون تغییر آن بازیابی می‌کنند (خواندن).
    • معمولاً شامل یک Query Handler است که Query را دریافت کرده و داده‌ها را از یک مدل خواندنی بهینه شده بازیابی می‌کند.
    • مثال: GetOrderByIdQuery, GetProductListQuery.

مزایای CQRS:

  1. مقیاس‌پذیری مستقل (Independent Scaling):
    • عملیات خواندن و نوشتن می‌توانند به صورت مستقل از یکدیگر مقیاس شوند. از آنجا که عملیات خواندن معمولاً بسیار بیشتر از نوشتن است، می‌توان مدل خواندنی را به صورت جداگانه مقیاس کرد.
  2. بهینه‌سازی عملکرد (Performance Optimization):
    • می‌توان مدل‌های داده و دیتابیس‌های مختلفی را برای خواندن و نوشتن استفاده کرد. مثلاً برای نوشتن از یک دیتابیس رابطه‌ای و برای خواندن از یک دیتابیس NoSQL یا یک denormalized view استفاده کرد.
  3. انعطاف‌پذیری بیشتر (Increased Flexibility):
    • مدل نوشتن (Domain Model) می‌تواند بر روی منطق کسب‌وکار پیچیده تمرکز کند، در حالی که مدل خواندن می‌تواند برای نیازهای خاص رابط کاربری بهینه شود.
  4. پشتیبانی از Event Sourcing:
    • CQRS به خوبی با Event Sourcing ترکیب می‌شود. Commands رویدادها را تولید می‌کنند و این رویدادها برای ساخت مدل‌های خواندنی استفاده می‌شوند.
  5. جداسازی مسئولیت‌ها (Separation of Concerns):
    • مسئولیت‌های خواندن و نوشتن به وضوح از یکدیگر جدا می‌شوند، که منجر به کدی تمیزتر و قابل نگهداری‌تر می‌شود.

معایب CQRS:

  1. پیچیدگی بیشتر: پیاده‌سازی CQRS پیچیدگی بیشتری را به سیستم اضافه می‌کند، به خصوص در مدیریت eventual consistency بین مدل‌های خواندن و نوشتن.
  2. نیاز به تیم با تجربه: برای پیاده‌سازی موفق CQRS، نیاز به تیمی با درک عمیق از الگوهای معماری و DDD است.

چه زمانی از CQRS استفاده می‌کنیم؟

CQRS برای سیستم‌هایی مناسب است که:

سوال 17: Event Sourcing در DDD چیست و چه مزایایی دارد؟

پاسخ: Event Sourcing (منبع رویداد) یک الگوی معماری است که در آن به جای ذخیره کردن تنها حالت فعلی یک Aggregate، تمام تغییرات حالت آن Aggregate به صورت دنباله‌ای از Domain Event ها ذخیره می‌شوند. این رویدادها به ترتیب زمانی ذخیره می‌شوند و حالت فعلی Aggregate با بازپخش این رویدادها از ابتدا تا انتها بازسازی می‌شود.

اجزای اصلی Event Sourcing:

  1. Event Store (مخزن رویداد): یک دیتابیس فقط-افزودنی (append-only) که تمام Domain Event ها را به ترتیب زمانی ذخیره می‌کند.
  2. Domain Events: اشیائی که تغییرات حالت Aggregate را نشان می‌دهند.
  3. Aggregate: مسئول تولید و اعمال Domain Event ها.

مزایای Event Sourcing:

  1. تاریخچه کامل (Full History):
    • تمام تغییرات حالت سیستم به صورت کامل و غیرقابل تغییر ذخیره می‌شوند. این امکان را فراهم می‌کند که در هر لحظه، حالت سیستم را در گذشته بازسازی کنیم.
    • برای Audit Logging و ردیابی تغییرات بسیار مفید است.
  2. پشتیبانی از CQRS:
    • Event Sourcing به طور طبیعی با CQRS ترکیب می‌شود. Event Store به عنوان مدل نوشتن عمل می‌کند و Domain Event ها برای ساخت مدل‌های خواندنی (Read Models) استفاده می‌شوند.
  3. رفع مشکلات همزمانی (Concurrency Issues):
    • از آنجا که Event Store فقط-افزودنی است، مشکلات همزمانی در نوشتن به حداقل می‌رسد.
  4. انعطاف‌پذیری در تغییرات مدل (Flexibility in Model Changes):
    • اگر نیاز به تغییر مدل دامنه یا اضافه کردن Read Model های جدید باشد، می‌توان رویدادهای گذشته را بازپخش کرد و مدل‌های جدید را از ابتدا ساخت.
  5. قابلیت Debugging و تحلیل بهتر:
    • با داشتن تاریخچه کامل رویدادها، Debugging و تحلیل رفتار سیستم بسیار آسان‌تر می‌شود.

معایب Event Sourcing:

  1. پیچیدگی بیشتر: پیاده‌سازی Event Sourcing پیچیدگی قابل توجهی را به سیستم اضافه می‌کند.
  2. Eventual Consistency: مدل‌های خواندنی ممکن است بلافاصله پس از تغییر حالت سیستم به‌روز نشوند و نیاز به eventual consistency داشته باشند.
  3. Querying Challenges: پرس‌وجو مستقیم از Event Store دشوار است، به همین دلیل نیاز به Read Models جداگانه است.

چه زمانی از Event Sourcing استفاده می‌کنیم؟

Event Sourcing برای سیستم‌هایی مناسب است که:

سوال 18: Unit of Work در DDD چیست و چه نقشی دارد؟

پاسخ: Unit of Work (واحد کار) یک الگوی طراحی است که تمام عملیات‌های دیتابیس (افزودن، به‌روزرسانی، حذف) را که در یک تراکنش واحد انجام می‌شوند، ردیابی می‌کند. هدف اصلی Unit of Work اطمینان از اتمی بودن (Atomicity) و سازگاری (Consistency) تغییرات در دیتابیس است.

نقش Unit of Work:

  1. ردیابی تغییرات: Unit of Work تمام اشیائی را که در طول یک عملیات کسب‌وکار تغییر کرده‌اند، ردیابی می‌کند.
  2. هماهنگی ذخیره‌سازی: در پایان عملیات، Unit of Work مسئولیت ذخیره تمام تغییرات ردیابی شده را به صورت یک تراکنش واحد بر عهده دارد. این بدان معناست که یا تمام تغییرات با موفقیت ذخیره می‌شوند یا هیچ یک از آن‌ها ذخیره نمی‌شوند.
  3. کاهش تعداد عملیات دیتابیس: به جای اینکه هر تغییر بلافاصله به دیتابیس ارسال شود، Unit of Work تغییرات را جمع‌آوری کرده و در یک مرحله واحد آن‌ها را ذخیره می‌کند، که می‌تواند عملکرد را بهبود بخشد.
  4. حفظ Consistency: با تضمین اینکه تمام تغییرات در یک تراکنش واحد انجام می‌شوند، Unit of Work به حفظ consistency داده‌ها کمک می‌کند.

چرا از Unit of Work استفاده می‌کنیم؟

  1. مدیریت تراکنش‌ها: برای مدیریت تراکنش‌های دیتابیس به صورت شفاف و خودکار.
  2. کاهش پیچیدگی: پنهان کردن جزئیات مدیریت تراکنش از لایه دامنه و لایه کاربرد.
  3. بهبود عملکرد: با دسته‌بندی عملیات دیتابیس و ارسال آن‌ها به صورت یکجا.

رابطه با Repository:

Repository ها مسئولیت بازیابی و ذخیره‌سازی Aggregate Root ها را بر عهده دارند، اما خودشان مسئولیت مدیریت تراکنش‌ها را ندارند. اینجاست که Unit of Work وارد می‌شود. Repository ها معمولاً از یک Unit of Work برای انجام عملیات ذخیره‌سازی استفاده می‌کنند.

مثال:

public interface IUnitOfWork
{
    void Commit();
    void Rollback();
}

// پیاده‌سازی در Entity Framework Core
public class EfCoreUnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _dbContext;

    public EfCoreUnitOfWork(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void Commit()
    {
        _dbContext.SaveChanges();
    }

    public void Rollback()
    {
        // در EF Core، تغییرات ردیابی شده تا زمانی که SaveChanges فراخوانی نشود، به دیتابیس اعمال نمی‌شوند.
        // برای Rollback واقعی، باید تغییرات ردیابی شده را پاک کرد یا DbContext را دوباره ایجاد کرد.
        // این مثال ساده است و ممکن است نیاز به پیاده‌سازی پیچیده‌تری داشته باشد.
    }
}

// استفاده در Application Service
public class OrderApplicationService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public OrderApplicationService(IOrderRepository orderRepository, IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

    public void CreateOrder(CreateOrderCommand command)
    {
        var order = Order.Create(Guid.NewGuid(), command.CustomerId);
        _orderRepository.Add(order);

        _unitOfWork.Commit(); // ذخیره تمام تغییرات ردیابی شده در این تراکنش
    }
}

در این مثال، Application Service از Unit of Work برای تضمین اینکه عملیات Add بر روی OrderRepository به صورت اتمی ذخیره می‌شود، استفاده می‌کند.

سوال 19: Specification در DDD چیست و چه کاربردی دارد؟

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

کاربردهای اصلی Specification:

  1. فیلتر کردن (Filtering):
    • برای انتخاب زیرمجموعه‌ای از اشیاء دامنه از یک مجموعه بزرگتر.
    • مثال: انتخاب تمام سفارشات تأیید شده، انتخاب مشتریانی که بیش از 1000 دلار خرید کرده‌اند.
  2. اعتبارسنجی (Validation):
    • برای بررسی اینکه آیا یک شیء دامنه خاص، یک قانون کسب‌وکار را رعایت می‌کند یا خیر.
    • مثال: آیا یک محصول برای فروش آنلاین آماده است؟ آیا یک کاربر شرایط لازم برای تخفیف را دارد؟
  3. ایجاد اشیاء جدید (Creating New Objects):
    • در برخی موارد، Specification می‌تواند برای تعیین اینکه آیا یک شیء جدید باید ایجاد شود یا خیر، استفاده شود.

ویژگی‌های Specification:

  1. قابل ترکیب (Composable): Specification ها را می‌توان با استفاده از عملگرهای منطقی (AND, OR, NOT) با یکدیگر ترکیب کرد تا قوانین پیچیده‌تری ایجاد شود.
  2. قابل استفاده مجدد (Reusable): یک Specification می‌تواند در چندین مکان در سیستم استفاده شود.
  3. خوانایی بالا (High Readability): Specification ها قوانین کسب‌وکار را به صورت واضح و قابل فهم بیان می‌کنند.
  4. تست‌پذیری (Testability): هر Specification می‌تواند به صورت مستقل تست شود.

مثال:

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T entity);
    // برای استفاده با ORM ها مانند EF Core
    Expression<Func<T, bool>> ToExpression();
}

public class ActiveCustomersSpecification : ISpecification<Customer>
{
    public bool IsSatisfiedBy(Customer customer)
    {
        return customer.IsActive && customer.LastActivityDate > DateTime.UtcNow.AddMonths(-6);
    }

    public Expression<Func<Customer, bool>> ToExpression()
    {
        return customer => customer.IsActive && customer.LastActivityDate > DateTime.UtcNow.AddMonths(-6);
    }
}

public class GoldCustomersSpecification : ISpecification<Customer>
{
    public bool IsSatisfiedBy(Customer customer)
    {
        return customer.TotalPurchases > 5000;
    }

    public Expression<Func<Customer, bool>> ToExpression()
    {
        return customer => customer.TotalPurchases > 5000;
    }
}

// ترکیب Specification ها
public class GoldAndActiveCustomersSpecification : ISpecification<Customer>
{
    private readonly ActiveCustomersSpecification _activeSpec = new ActiveCustomersSpecification();
    private readonly GoldCustomersSpecification _goldSpec = new GoldCustomersSpecification();

    public bool IsSatisfiedBy(Customer customer)
    {
        return _activeSpec.IsSatisfiedBy(customer) && _goldSpec.IsSatisfiedBy(customer);
    }

    public Expression<Func<Customer, bool>> ToExpression()
    {
        return customer => _activeSpec.ToExpression().Compile()(customer) && _goldSpec.ToExpression().Compile()(customer);
    }
}

// استفاده در Repository
public class CustomerRepository : ICustomerRepository
{
    private readonly ApplicationDbContext _dbContext;

    public CustomerRepository(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IEnumerable<Customer> Find(ISpecification<Customer> specification)
    {
        return _dbContext.Customers.Where(specification.ToExpression()).ToList();
    }
}

در این مثال، Specification ها برای فیلتر کردن مشتریان فعال و مشتریان طلایی استفاده می‌شوند و می‌توانند با هم ترکیب شوند. این باعث می‌شود منطق فیلتر کردن از Repository جدا شود و قابل استفاده مجدد باشد.

سوال 20: Anti-Corruption Layer (ACL) در DDD چیست و چه زمانی از آن استفاده می‌کنیم؟

پاسخ: Anti-Corruption Layer (ACL) یک الگوی طراحی در Domain-Driven Design (DDD) است که به عنوان یک لایه ترجمه بین یک Bounded Context و یک سیستم خارجی (مانند یک سیستم قدیمی، یک سرویس شخص ثالث، یا یک Bounded Context دیگر با مدل دامنه متفاوت) عمل می‌کند. هدف اصلی ACL محافظت از مدل دامنه تمیز و غنی Bounded Context شما در برابر نفوذ مدل‌های خارجی و ناسازگار است.

چرا به ACL نیاز داریم؟

  1. محافظت از مدل دامنه: در سیستم‌های بزرگ، اغلب نیاز به تعامل با سیستم‌های خارجی داریم که مدل‌های داده و مفاهیم متفاوتی دارند. بدون ACL، این مدل‌های خارجی می‌توانند به مدل دامنه شما نفوذ کرده و آن را آلوده کنند، که منجر به پیچیدگی، ابهام و دشواری در نگهداری می‌شود.
  2. ترجمه مدل‌ها: ACL مسئولیت ترجمه داده‌ها و عملیات بین مدل دامنه شما و مدل سیستم خارجی را بر عهده دارد. این ترجمه می‌تواند دو طرفه باشد: تبدیل داده‌های ورودی از سیستم خارجی به مدل دامنه شما و تبدیل داده‌های خروجی از مدل دامنه شما به فرمت مورد نیاز سیستم خارجی.
  3. کاهش کوپلینگ: ACL با جداسازی Bounded Context شما از جزئیات پیاده‌سازی سیستم خارجی، کوپلینگ را کاهش می‌دهد. اگر سیستم خارجی تغییر کند، فقط ACL نیاز به به‌روزرسانی دارد و مدل دامنه شما تحت تأثیر قرار نمی‌گیرد.
  4. مدیریت تغییرات: ACL به عنوان یک بافر عمل می‌کند که تغییرات در سیستم خارجی را از مدل دامنه شما جدا می‌کند.

پیاده‌سازی ACL:

ACL معمولاً به صورت یک لایه از کلاس‌ها و واسط‌ها پیاده‌سازی می‌شود که شامل موارد زیر است:

مثال:

فرض کنید یک سیستم جدید مدیریت سفارشات (Order Management Bounded Context) در حال توسعه است و نیاز به تعامل با یک سیستم قدیمی مدیریت مشتریان (Legacy Customer System) دارد. مدل مشتری در سیستم قدیمی ممکن است با مدل مشتری در سیستم جدید متفاوت باشد.

// مدل مشتری در Bounded Context جدید
public class Customer
{
    public Guid Id { get; private set; }
    public string FullName { get; private set; }
    public string EmailAddress { get; private set; }
}

// مدل مشتری در سیستم قدیمی (Legacy System)
public class LegacyCustomerDto
{
    public string CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

// Anti-Corruption Layer
public class LegacyCustomerAdapter
{
    private readonly ILegacyCustomerService _legacyCustomerService;

    public LegacyCustomerAdapter(ILegacyCustomerService legacyCustomerService)
    {
        _legacyCustomerService = legacyCustomerService;
    }

    public Customer GetCustomerById(string legacyCustomerId)
    {
        var legacyCustomerDto = _legacyCustomerService.GetCustomerDetails(legacyCustomerId);
        if (legacyCustomerDto == null) return null;

        // ترجمه از مدل قدیمی به مدل جدید
        return new Customer(
            Guid.Parse(legacyCustomerDto.CustomerId), // فرض کنید CustomerId در Legacy System یک GUID است
            $"{legacyCustomerDto.FirstName} {legacyCustomerDto.LastName}",
            legacyCustomerDto.Email
        );
    }

    // متدهای دیگر برای تبدیل و تعامل
}

در این مثال، LegacyCustomerAdapter به عنوان ACL عمل می‌کند. این کلاس مسئولیت تبدیل LegacyCustomerDto (مدل سیستم قدیمی) به Customer (مدل دامنه جدید) را بر عهده دارد و از نفوذ مدل قدیمی به مدل دامنه جدید جلوگیری می‌کند.