عنوان:

‫Domain-Driven Design (DDD)؛ بخش دوازهم: سرویس‌های دامنه


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۰۷ ۰۸:۱۰
آدرس: www.dntips.ir
Domain Services: قهرمانان خاموش در دنیای دامنه 🦸‍♂️

تا به حال به این فکر کرده‌اید که چه اتفاقی می‌افتد وقتی یک منطق تجاری، به تنهایی به یک Aggregate یا Entity خاص تعلق ندارد؟ فرض کنید در یک سیستم بانکی، می‌خواهید پولی را از یک حساب به حساب دیگر انتقال دهید.
  • آیا این منطق باید در کلاس Account (حساب مبدا) قرار گیرد؟ خیر، زیرا به یک حساب دیگر هم مربوط است.
  • آیا باید در کلاس Transaction قرار گیرد؟ خیر، زیرا منطق انتقال، فراتر از یک تراکنش ساده است.
در این موارد، منطق به یک سرویس دامنه (Domain Service) تعلق دارد.

Domain Service چیست؟

Domain Service یک سرویس بدون وضعیت (Stateless) است که عملیات مربوط به چندین Aggregate را هماهنگ می‌کند. این سرویس، یک واسط (Interface) برای عملیات‌هایی است که به صورت طبیعی در هیچ یک از Entities یا Value Objects قرار نمی‌گیرند.
به عبارت ساده، Domain Service یک "هماهنگ‌کننده" است که منطق‌هایی را در خود نگه می‌دارد که از مرز یک Aggregate فراتر می‌روند.

چرا به Domain Service نیاز داریم؟

استفاده از Domain Service، چندین مزیت کلیدی دارد:
  • حفظ کپسوله‌سازی: با قرار دادن منطق در Domain Service، از آلوده شدن Aggregate ها با منطق نامربوط جلوگیری می‌کنیم. این کار باعث می‌شود Aggregate ها ساده و متمرکز باقی بمانند.
  • ایجاد یک لایه انتزاعی: Domain Service یک لایه انتزاعی ایجاد می‌کند که به ما امکان می‌دهد تا به جای جزئیات پیاده‌سازی، روی مفهوم اصلی (مثلاً "انتقال وجه") تمرکز کنیم.
  • تست‌پذیری: Domain Service ها به دلیل نداشتن وضعیت، به راحتی قابل تست هستند.

مثال عملی:
بیایید به مثال انتقال وجه بازگردیم. یک Domain Service به نام TransferService می‌تواند این عملیات را مدیریت کند.
public class TransferService
{
    private readonly IAccountRepository _accountRepository;

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

    public async Task Transfer(int fromAccountId, int toAccountId, decimal amount)
    {
        var fromAccount = await _accountRepository.GetByIdAsync(fromAccountId);
        var toAccount = await _accountRepository.GetByIdAsync(toAccountId);

        if (fromAccount == null || toAccount == null)
        {
            throw new ArgumentException("One of the accounts does not exist.");
        }

        fromAccount.Withdraw(amount);
        toAccount.Deposit(amount);

        // Here, the repository would save changes
    }
}
در این مثال، کلاس TransferService از دو Account Aggregate استفاده می‌کند و منطق اصلی انتقال را در خود دارد. این سرویس، وابستگی‌ها را به شکل درستی مدیریت می‌کند و منطق را در یک مکان متمرکز نگه می‌دارد.

به این فکر کنید: در پروژه‌تان، آیا متدهایی دارید که منطق مربوط به چندین کلاس را در خود دارند؟ آیا می‌توانید آن‌ها را به یک Domain Service مستقل منتقل کنید تا کد شما تمیزتر و قابل نگهداری‌تر شود؟
Domain Service ها، ابزارهای قدرتمندی برای مدیریت منطق‌های پیچیده‌ای هستند که از مرز Aggregate ها فراتر می‌روند. آن‌ها به ما کمک می‌کنند تا مدل دامنه‌مان را تمیز، متمرکز و خوانا نگه داریم و در عین حال، تمام قوانین تجاری را به درستی پیاده‌سازی کنیم.


Application Services: پل ارتباطی بین دنیای بیرون و مدل دامنه 🌉

تا به حال به این فکر کرده‌اید که چگونه یک درخواست از سمت کاربر (مثلاً کلیک روی دکمه "ثبت سفارش") به مدل دامنه‌ی پیچیده شما در پشت صحنه مرتبط می‌شود؟ آیا منطق UI باید مستقیماً با Aggregates یا Repositories صحبت کند؟ قطعا خیر، زیرا این کار باعث ایجاد وابستگی‌های سفت و سخت می‌شود و لایه‌های مختلف را در هم می‌آمیزد.
در DDD، ما از یک لایه میانی به نام Application Services (سرویس‌های کاربردی) استفاده می‌کنیم تا این ارتباط را به صورت سالم و مدیریت‌شده برقرار کنیم.

Application Service چیست؟

Application Service یک لایه نازک است که به عنوان یک هماهنگ‌کننده عمل می‌کند. این سرویس، یک واسط (Interface) برای دنیای بیرون (مثلاً کنترلرها در ASP.NET Core) فراهم می‌کند و تمام فرآیندهای مربوط به یک عملیات خاص را مدیریت می‌نماید.
وظیفه اصلی Application Service این است که:
  • دریافت ورودی از لایه ارائه (Presentation Layer) یا UI.
  • فراخوانی منطق دامنه (Domain Logic) برای اجرای عملیات.
  • مدیریت تراکنش‌ها و ذخیره تغییرات.
  • بازگرداندن نتیجه به دنیای بیرون.
این سرویس هیچ منطق تجاری (Business Logic) پیچیده‌ای را در خود ندارد. تمام منطق، در داخل Domain Model (مدل دامنه) و Domain Services قرار دارد.

Application Service در برابر Domain Service

تفاوت بین این دو نوع سرویس بسیار مهم است:
Application Service:
  • محل: در لایه کاربردی (Application Layer).
  • مسئولیت: مدیریت فرآیندها، هماهنگی با Repository ها و Domain Services و مدیریت تراکنش‌ها.
  • زبان: با زبان کاربرد (Application Language) صحبت می‌کند (مثلاً CreateOrder یا RegisterUser).
Domain Service:
  • محل: در لایه دامنه (Domain Layer).
  • مسئولیت: اجرای منطق تجاری مربوط به چندین Aggregate.
  • زبان: با زبان مشترک (Ubiquitous Language) دامنه صحبت می‌کند (مثلاً TransferFunds یا CalculateShippingCost).

مثال عملی:

فرض کنید یک API برای ثبت سفارش جدید داریم.
Application Service:
public class OrderService
{
    private readonly IOrderRepository _orderRepository;

    public OrderService(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task CreateOrder(CreateOrderCommand command)
    {
        var order = new Order(command.CustomerId, command.ProductId, command.Quantity);

        // This method contains no business logic
        await _orderRepository.AddAsync(order);
    }
}
Application Controller:
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrderController(OrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpPost("create")]
    public async Task<IActionResult> Create([FromBody] CreateOrderCommand command)
    {
        await _orderService.CreateOrder(command);
        return Ok();
    }
}
در این مدل، OrderController هیچ اطلاعی از جزئیات Repository و Domain Model ندارد و فقط با OrderService صحبت می‌کند. این جداسازی، باعث می‌شود که هر لایه بر روی مسئولیت‌های خود تمرکز کند و کد شما تمیزتر و قابل نگهداری‌تر باشد.

به این فکر کنید: آیا در پروژه‌تان، کنترلرها یا Endpoint ها مستقیماً با Repository ها صحبت می‌کنند؟ چگونه می‌توانید یک Application Service را به عنوان واسط بین آن‌ها قرار دهید تا کدتان ساختار یافته‌تر شود؟
Application Service ها، یک لایه حیاتی برای جداسازی مسئولیت‌ها هستند. آن‌ها به ما کمک می‌کنند تا یک خط جداکننده واضح بین منطق تجاری و لایه‌های فنی مانند UI یا دیتابیس ایجاد کنیم. این کار، ساخت نرم‌افزارهای پیچیده را به یک فرآیند قابل مدیریت تبدیل می‌کند.

نظرات

  • وحید نصیری در ۱۴۰۴/۰۶/۰۷ ۰۹:۵۱
    یک مثال از Domain Services و Application Services در سیستم پرداخت شارژ آپارتمان

    تصور کنید در حال قدم زدن در یک ساختمان آپارتمانی هستید، جایی که هر طبقه و هر واحد، بخشی از یک سیستم پیچیده اما هماهنگ را تشکیل می‌دهد. حالا فکر کنید اگر بخواهیم این ساختمان را به یک نرم‌افزار تبدیل کنیم – مثلاً سیستمی برای مدیریت و پرداخت شارژ ماهانه واحدها. چه چیزی این سیستم را زنده و کارآمد نگه می‌دارد؟ آیا فقط داده‌ها و دیتابیس کافی‌اند، یا نیاز به لایه‌هایی داریم که منطق واقعی زندگی در این ساختمان را بازتاب دهند؟
    در Domain-Driven Design (DDD)، که یک رویکرد طراحی نرم‌افزار است، تمرکز روی “دامنه” (Domain) است – همان هسته اصلی مشکل، مثل قوانین پرداخت شارژ در یک آپارتمان. اینجا، نه فقط کد می‌نویسیم، بلکه جهان واقعی را مدل‌سازی می‌کنیم. دو مفهوم کلیدی در این جهان، Domain Services و Application Services هستند. Domain Services مثل قوانین نانوشته ساختمان عمل می‌کنند: منطق خالص دامنه، بدون وابستگی به دنیای بیرون. Application Services اما، مثل مدیر ساختمان هستند: هماهنگ‌کننده جریان کارها، که از دامنه استفاده می‌کنند و با ابزارهایی مثل دیتابیس (اینجا EF-Core) ارتباط برقرار می‌کنند.

    فرض کنید در حال طراحی سیستمی هستیم برای پرداخت شارژ آپارتمان. ساکنان واحدها باید شارژ ماهانه‌شان را بپردازند، که بر اساس متراژ واحد، هزینه‌های مشترک (مثل آب و برق) و شاید جریمه‌های دیرکرد محاسبه می‌شود. شاید در ذهنتان سؤال‌هایی شکل بگیرد: “اگر منطق محاسبه شارژ پیچیده‌تر شود، چه؟” عالی، همین سؤال‌ها ما را جلو می‌برند.

    گام اول: مدل‌سازی دامنه – جایی که داستان شروع می‌شود
    ابتدا، فکر کنید به موجودیت‌های اصلی (Entities) در این دامنه. یک Apartment (واحد آپارتمانی) داریم، با Resident (ساکن)، Charge (شارژ ماهانه) و Payment (پرداخت). این‌ها نه فقط کلاس‌های ساده، بلکه اشیایی با رفتار هستند. مثلاً Charge می‌تواند خودش را محاسبه کند، اما اگر محاسبه نیاز به هماهنگی بین چند موجودیت داشته باشد، Domain Service وارد میدان می‌شود.
    در DDD از Domain Services برای عملیات‌هایی استفاده می‌شوند که به یک موجودیت خاص تعلق ندارند، اما بخشی از منطق دامنه‌اند. مثلاً محاسبه شارژ کلی ساختمان، که نیاز به جمع‌آوری داده از همه واحدها دارد. این سرویس‌ها خالص‌اند: هیچ وابستگی به دیتابیس یا وب‌سرویس ندارند. فقط منطق دامنه.
    حالا تصور کنید کد زیر را در لایه Domain بنویسیم (در C#، با تمرکز روی DDD):
    // در namespace Domain.Entities
    public class Apartment
    {
        public Guid Id { get; private set; }
        public double Area { get; private set; } // متراژ واحد
        public List<Charge> Charges { get; private set; } = new List<Charge>();
    
        public Apartment(double area)
        {
            Id = Guid.NewGuid();
            Area = area;
        }
    
        public void AddCharge(Charge charge)
        {
            Charges.Add(charge);
        }
    }
    
    public class Charge
    {
        public Guid Id { get; private set; }
        public DateTime Month { get; private set; }
        public double Amount { get; private set; } // مبلغ شارژ
        public bool IsPaid { get; private set; }
    
        public Charge(DateTime month, double amount)
        {
            Id = Guid.NewGuid();
            Month = month;
            Amount = amount;
            IsPaid = false;
        }
    
        public void MarkAsPaid()
        {
            IsPaid = true;
        }
    }
    
    // حالا Domain Service: ChargeCalculationService
    // این سرویس منطق محاسبه شارژ را مدیریت می‌کند، بدون وابستگی به ذخیره‌سازی
    public interface IChargeCalculationService
    {
        double CalculateMonthlyCharge(Apartment apartment, double commonCosts, double penaltyRate);
    }
    
    public class ChargeCalculationService : IChargeCalculationService
    {
        public double CalculateMonthlyCharge(Apartment apartment, double commonCosts, double penaltyRate)
        {
            // منطق دامنه: شارژ = (متراژ * نرخ پایه) + سهم هزینه‌های مشترک + جریمه اگر دیرکرد داشته باشد
            double baseCharge = apartment.Area * 1000; // فرض نرخ پایه 1000 تومان per متر
            double shareOfCommon = commonCosts / /* تعداد واحدها، که می‌تواند از repository بیاید اما اینجا ساده */ 10;
            double penalty = apartment.Charges.Any(c => !c.IsPaid && c.Month < DateTime.Now.AddMonths(-1)) ? baseCharge * penaltyRate : 0;
            
            return baseCharge + shareOfCommon + penalty;
        }
    }
    در اینجا ChargeCalculationService فقط روی منطق تمرکز دارد. هیچ صحبتی از دیتابیس نیست. حالا فکر کنید: اگر قوانین شارژ تغییر کند (مثل اضافه شدن مالیات)، کجا باید تغییر دهیم؟ دقیقاً اینجا، در قلب دامنه. این جداسازی، سیستم را انعطاف‌پذیر می‌کند – انگار ساختمان‌تان را طوری ساخته‌اید که دیوارها را بدون تخریب کل بنا جابه‌جا کنید.

    گام دوم: ورود به دنیای کاربرد – Application Services و EF-Core
    حالا که دامنه‌مان محکم است، نیاز به لایه‌ای داریم که این دامنه را به دنیای واقعی وصل کند. اینجا Application Services وارد می‌شوند. این سرویس‌ها جریان اپلیکیشن را مدیریت می‌کنند: ورودی‌ها را می‌گیرند، از دامنه استفاده می‌کنند، و خروجی‌ها را برمی‌گردانند. مثلاً وقتی ساکنی می‌خواهد شارژش را بپردازد، Application Service مسئولیت هماهنگی را بر عهده می‌گیرد – از جمله ذخیره تغییرات در دیتابیس با EF-Core.
    Application Services معمولاً در لایه Application قرار می‌گیرند و از Repositoryها (برای دسترسی به داده) و Domain Services استفاده می‌کنند. EF-Core اینجا نقش persistence را بازی می‌کند: یک ORM قدرتمند که مدل‌های دامنه را به جدول‌های دیتابیس map می‌کند.
    بیایید یک Application Service تصور کنیم: PaymentApplicationService. این سرویس پرداخت را پردازش می‌کند، شارژ را محاسبه می‌کند (با کمک Domain Service)، و سپس تغییرات را ذخیره می‌کند.
    ابتدا، یک Repository ساده برای Apartment:
    // در namespace Infrastructure.Persistence (با EF-Core)
    public class ApartmentRepository : IApartmentRepository
    {
        private readonly ApartmentDbContext _context; // DbContext EF-Core
    
        public ApartmentRepository(ApartmentDbContext context)
        {
            _context = context;
        }
    
        public async Task<Apartment> GetByIdAsync(Guid id)
        {
            return await _context.Apartments.Include(a => a.Charges).FirstOrDefaultAsync(a => a.Id == id);
        }
    
        public async Task SaveChangesAsync()
        {
            await _context.SaveChangesAsync();
        }
    }
    
    // DbContext نمونه:
    public class ApartmentDbContext : DbContext
    {
        public DbSet<Apartment> Apartments { get; set; }
        public DbSet<Charge> Charges { get; set; }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("your-connection-string"); // یا هر دیتابیس دیگر
        }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Config برای mapping، مثل Owned Types اگر لازم
            modelBuilder.Entity<Apartment>().OwnsMany(a => a.Charges);
        }
    }
    حالا Application Service:
    // در namespace Application.Services
    public interface IPaymentApplicationService
    {
        Task ProcessPaymentAsync(Guid apartmentId, double paymentAmount);
    }
    
    public class PaymentApplicationService : IPaymentApplicationService
    {
        private readonly IApartmentRepository _repository;
        private readonly IChargeCalculationService _chargeCalculator; // تزریق Domain Service
    
        public PaymentApplicationService(IApartmentRepository repository, IChargeCalculationService chargeCalculator)
        {
            _repository = repository;
            _chargeCalculator = chargeCalculator;
        }
    
        public async Task ProcessPaymentAsync(Guid apartmentId, double paymentAmount)
        {
            var apartment = await _repository.GetByIdAsync(apartmentId);
            if (apartment == null) throw new Exception("واحد یافت نشد!");
    
            // استفاده از Domain Service برای محاسبه
            double dueCharge = _chargeCalculator.CalculateMonthlyCharge(apartment, 500000, 0.05); // commonCosts=500k, penalty=5%
    
            if (paymentAmount < dueCharge) throw new Exception("مبلغ ناکافی!");
    
            // ایجاد Charge جدید و مارک پرداخت
            var newCharge = new Charge(DateTime.Now, dueCharge);
            newCharge.MarkAsPaid();
            apartment.AddCharge(newCharge);
    
            // ذخیره با EF-Core
            await _repository.SaveChangesAsync();
        }
    }
    ببینید چطور Application Service مثل یک پل عمل می‌کند: ورودی (paymentAmount) را می‌گیرد، از دامنه (محاسبه) استفاده می‌کند، و با EF-Core ذخیره می‌کند.
    نکته‌ی مهم: اگر بخواهیم این سیستم را تست کنیم، چقدر آسان است؟ Domain Service را می‌توانیم مستقل تست کنیم، بدون دیتابیس. این جداسازی، کلید DDD است.

    چند پرسش و پاسخ برای درک بهتر موضوع

    سؤال: اگر بخواهیم شارژ هر واحد را بر اساس متراژ یا مساوی محاسبه کنیم، این منطق کجا باید برود؟
    • نه در Entity (چون مربوط به یک واحد نیست، بلکه به کل ساختمان مربوط است).
    • نه در Application Service (چون بخشی از منطق دامنه است، نه صرفاً هماهنگی).
    پس نیاز به Domain Service داریم.

    سؤال: چه کسی مسئول است که سرویس محاسبه شارژ را صدا بزند، خروجی را بگیرد، و در دیتابیس (از طریق EF-Core) ذخیره کند؟
    این همان Application Service است.

    Application Service مانند یک رهبر ارکستر است: هماهنگ می‌کند.
    Domain Service مانند یک نوازنده متخصص است: محاسبات تخصصی را انجام می‌دهد.

    چند سؤال:
    • اگر کل ساختمان فقط یک قانون ساده «تقسیم مساوی» داشت، آیا باز هم Domain Service لازم بود؟
    • اگر قرار بود علاوه بر محاسبه شارژ، پیامکی هم برای ساکنان ارسال شود، این وظیفه در Application Service قرار می‌گیرد یا Domain Service؟
    پاسخ‌ها:
    • هر جا منطق «دامنه» بود (مثل فرمول تقسیم شارژ)، باید در Domain Service بماند.
    • هر جا «هماهنگی با جهان بیرون» بود (مثل دیتابیس یا SMS)، در Application Service است.

    بنابراین
    • Domain Service = مغز محاسباتی و منطقی دامنه.
    • Application Service = هماهنگ‌کننده عملیات و استفاده از Domain.
    اینجا به شکل ساده دیدیم که چطور در یک سیستم شارژ آپارتمان، این دو نقش مکمل هم می‌شوند.