Внедрение доменного сервиса в AggregateRoots в DDD
Общепринятый совет в DDD состоит в том, что Aggregate Roots не используют доменную службу. Доменная служба должна координировать два Агрегированных Корня для достижения поведения.
Меня очень удивило, когда я увидел этот блог, написанный Ринатом Абдуллином с названием Building Blocks Of CQRS. В разделе "Служба домена" вы прочтете, что служба домена вводится в совокупный корень.
Может ли Aggregate Root принимать доменную службу?
4 ответа
Пожалуйста, не обращайте внимания на эту статью. Это было написано давным-давно и совершенно неправильно. При реализации модуля с шаблонами AggregateRoot и DomainService я бы рекомендовал иметь более высокую логику (например, обработчик запросов), которая отвечает за:
- Загрузка агрегата
- Выполнение расчетов с помощью доменных сервисов
- Мутируя агрегатное состояние соответственно.
В некотором смысле, да. Если AR действительно нужен сервис для выполнения какой-либо своей работы, вы можете добавить его в качестве аргумента метода. Если AR нуждается в сервисе для большей части своего поведения, то, вероятно, он неправильно смоделирован.
Я считаю следующее объяснение довольно хорошим. Он основан на книге Вона Вернона и "внедряет" доменную службу в модель домена посредством вызова метода, который действительно нуждается в этой услуге.
public class PurchaseOrder
{
public string Id { get; private set; }
public string VendorId { get; private set; }
public string PONumber { get; private set; }
public string Description { get; private set; }
public decimal Total { get; private set; }
public DateTime SubmissionDate { get; private set; }
public ICollection<Invoice> Invoices { get; private set; }
public decimal InvoiceTotal
{
get { return this.Invoices.Select(x => x.Amount).Sum(); }
}
public bool IsFullyInvoiced
{
get { return this.Total <= this.InvoiceTotal; }
}
bool ContainsInvoice(string vendorInvoiceNumber)
{
return this.Invoices.Any(x => x.VendorInvoiceNumber.Equals(
vendorInvoiceNumber, StringComparison.OrdinalIgnoreCase));
}
public Invoice Invoice(IInvoiceNumberGenerator generator,
string vendorInvoiceNumber, DateTime date, decimal amount)
{
// These guards maintain business integrity of the PO.
if (this.IsFullyInvoiced)
throw new Exception("The PO is fully invoiced.");
if (ContainsInvoice(vendorInvoiceNumber))
throw new Exception("Duplicate invoice!");
var invoiceNumber = generator.GenerateInvoiceNumber(
this.VendorId, vendorInvoiceNumber, date);
var invoice = new Invoice(invoiceNumber, vendorInvoiceNumber, date, amount);
this.Invoices.Add(invoice);
DomainEvents.Raise(new PurchaseOrderInvoicedEvent(this.Id, invoice.InvoiceNumber));
return invoice;
}
}
public class PurchaseOrderService
{
public PurchaseOrderService(IPurchaseOrderRepository repository,
IInvoiceNumberGenerator invoiceNumberGenerator)
{
this.repository = repository;
this.invoiceNumberGenerator = invoiceNumberGenerator;
}
readonly IPurchaseOrderRepository repository;
readonly IInvoiceNumberGenerator invoiceNumberGenerator;
public void Invoice(string purchaseOrderId,
string vendorInvoiceNumber, DateTime date, decimal amount)
{
// Transaction management, along with committing the unit of work
// can be moved to ambient infrastructure.
using (var ts = new TransactionScope())
{
var purchaseOrder = this.repository.Get(purchaseOrderId);
if (purchaseOrder == null)
throw new Exception("PO not found!");
purchaseOrder.Invoice(this.invoiceNumberGenerator,
vendorInvoiceNumber, date, amount);
this.repository.Commit();
ts.Complete();
}
}
}
Очень сложно внедрить что-либо в доменные объекты, и это зависит от конкретной технологии. В Java это требует компиляции аспектов времени в классы вашего домена. И хотя я могу ошибаться в этом, я думаю, что большинство лидеров DDD считают, что это, как правило, плохая идея. И Эванс, и Вернон оба активно препятствуют этому, и мне нравится их слушать. Для полного объяснения прочитайте Вернона.