Domain-Driven Design (DDD)؛ بخش دوازهم: سرویسهای دامنه
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۰۷ ۰۸:۱۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Account (حساب مبدا) قرار گیرد؟ خیر، زیرا به یک حساب دیگر هم مربوط است.Transaction قرار گیرد؟ خیر، زیرا منطق انتقال، فراتر از یک تراکنش ساده است.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 استفاده میکند و منطق اصلی انتقال را در خود دارد. این سرویس، وابستگیها را به شکل درستی مدیریت میکند و منطق را در یک مکان متمرکز نگه میدارد.CreateOrder یا RegisterUser).TransferFunds یا CalculateShippingCost).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);
}
}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 صحبت میکند. این جداسازی، باعث میشود که هر لایه بر روی مسئولیتهای خود تمرکز کند و کد شما تمیزتر و قابل نگهداریتر باشد.// در 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;
}
}// در 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);
}
}// در 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();
}
}