Chiến Lược Xác Định Aggregate Root, Entity Và Value Object Trong DDD

Khi áp dụng Domain-Driven Design (DDD), việc phân loại chính xác các đối tượng thành Aggregate Root, Entity hay Value Object là nền tảng để xây dựng mô hình miền dữ liệu vững chắc. Dưới đây là quy trình tiếp cận thực tế để thực hiện phân tích này:

1. Đánh giá Bản Chất Xác Nhận (Identity)

Tiêu chí đầu tiên cần xem xét là cách một đối tượng được nhận diện trong hệ thống:

  • Nếu đối tượng cần một mã định danh toàn cục xuyên suốt vòng đời và được nhiều module khác nhau tham chiếu, nó có khả năng cao là Aggregate Root.
  • Nếu đối tượng chỉ cần duy nhất trong phạm vi một nhóm cụ thể (ví dụ: một hàng hóa trong đơn mua sắm), nó phù hợp với vai trò Entity.
  • Nếu đối tượng được nhận biết hoàn toàn qua tập hợp thuộc tính của nó mà không cần mã ID riêng biệt, đây là dấu hiệu của Value Object.

2. Xác Định Biên Giới Giao Dịch (Consistency Boundaries)

Dữ liệu nào phải được cập nhật đồng bộ và nguyên tử? Các đối tượng chia sẻ cùng một yêu cầu nhất quán về trạng thái nên được gom vào chung một Aggregate. Aggregate Root sẽ đóng vai trò là cổng ra vào duy nhất, kiểm soát mọi thay đổi trạng thái của các thành viên bên trong để đảm bảo tính toàn vẹn dữ liệu.

3. Phân Tích Hành Vi Và Mức Độ Phụ Thuộc

Đối tượng có khả năng tự đưa ra quyết định nghiệp vụ hoặc điều phối quy trình thường được chọn làm Aggregate Root. Ngược lại, các đối tượng có hành vi bị ràng buộc chặt chẽ bởi ngữ cảnh cha mẹ hoặc chỉ đóng vai trò mô tả thông tin sẽ phù hợp hơn với Entity hoặc Value Object.

4. Quan Sát Mô Thức Biến Đổi Dữ Liệu

  • Tính bất biến (Immutability): Value Object thường được thiết kế là bất biến. Khi cần thay đổi thông tin, hệ thống sẽ tạo ra một thể hiện mới thay vì sửa đổi trực tiếp.
  • Tính linh hoạt trạng thái: Entity và Aggregate Root cho phép thay đổi trạng thái theo thời gian nhưng vẫn giữ nguyên bản chất định danh.

Minh họa thực tế trên nền tảng thương mại điện tử

  • Khách Hàng (Customer): Mang mã ID duy nhất, tồn tại độc lập và được nhiều quy trình khác nhau (tài khoản, hỗ trợ, marketing) tham chiếu. Đây là Aggregate Root.
  • Phiếu Đặt Hàng (PurchaseOrder): Có mã ID toàn cục, đóng gói nhiều sản phẩm con. Việc cập nhật trạng thái phiếu đặt hàng và danh sách sản phẩm trong nó phải diễn ra đồng bộ. Đây cũng là một Aggregate Root độc lập.
  • Thông Tin Giao Hàng (ShippingDetail): Bao gồm đường phố, quận huyện, mã bưu chính. Nó không có vòng đời độc lập mà luôn đi kèm với khách hàng hoặc đơn hàng. Thay đổi thông tin giao hàng không ảnh hưởng đến danh tính của đối tượng cha, nên được mô hình hóa là Value Object.

Sau khi xác định rõ các thành phần, thách thức tiếp theo là xử lý tương tác giữa các Aggregate Root khác nhau. Trong DDD, nguyên tắc vàng là không nên tham chiếu trực tiếp từ Aggregate này sang Aggregate khác. Có hai mẫu thiết kế phổ biến để giải quyết bài toán này:

Sử dụng Domain Service để Điều Khiển

Domain Service đóng vai trò trung gian, chịu trách nhiệm tải dữ liệu từ các Repository, thực hiện kiểm tra nghiệp vụ và phối hợp cập nhật trạng thái. Cách tiếp cận này tập trung hóa logic phức tạp, giúp các Aggregate Root chỉ tập trung vào quy tắc của chính nó.

Sơ đồ luồng Domain Service

Ưu điểm: Dễ dàng theo dõi luồng xử lý từ đầu đến cuối, code mạch lạc và thuận tiện cho việc debug.
Nhược điểm: Khi nghiệp vụ phát triển, service có thể trở nên cồng kềnh và khó mở rộng mà không làm rối logic hiện có.

public interface IOrderFulfillmentHandler
{
    Task ProcessShipmentRequestAsync(Guid purchaseId);
}

public class OrderFulfillmentHandler : IOrderFulfillmentHandler
{
    private readonly IQueryService _queryService;
    private readonly IUnitOfWork _unitOfWork;

    public OrderFulfillmentHandler(IQueryService queryService, IUnitOfWork unitOfWork)
    {
        _queryService = queryService;
        _unitOfWork = unitOfWork;
    }

    public async Task ProcessShipmentRequestAsync(Guid purchaseId)
    {
        var cartData = await _queryService.FetchCartDetailsAsync(purchaseId);
        
        foreach (var productEntry in cartData.Items)
        {
            var stockRecord = await _queryService.CheckStockAsync(productEntry.ProductId);
            
            if (!stockRecord.CanReserve(productEntry.RequestedQty))
            {
                throw new DomainException($"Không đủ tồn kho cho sản phẩm: {productEntry.ProductId}");
            }

            stockRecord.Reserve(productEntry.RequestedQty);
            await _unitOfWork.SaveAsync(stockRecord);
        }

        var mainOrder = await _queryService.LoadOrderAsync(purchaseId);
        mainOrder.ConfirmAndShip();
        await _unitOfWork.SaveAsync(mainOrder);
    }
}

Sử dụng Domain Event để Truyền Thông

Thay vì gọi trực tiếp, Aggregate Root sẽ phát ra một sự kiện khi trạng thái quan trọng thay đổi. Các module liên quan sẽ lắng nghe và phản hồi tương ứng. Mẫu này phá vỡ mối phụ thuộc chặt chẽ, thúc đẩy kiến trúc hướng sự kiện.

Sơ đồ luồng Domain Event

Ưu điểm: Tính mở rộng cao, dễ dàng thêm các tác vụ phụ trợ (như gửi email, cập nhật báo cáo) mà không động vào code gốc.
Nhược điểm: Luồng xử lý bị phân tán, việc theo dõi toàn cảnh nghiệp vụ trở nên phức tạp hơn.

public class CartConfirmedNotification : INotification
{
    public Guid PurchaseReference { get; }

    public CartConfirmedNotification(Guid purchaseReference)
    {
        PurchaseReference = purchaseReference;
    }
}

public class PurchaseCart : Entity
{
    public Guid CartId { get; }
    public IReadOnlyCollection<CartItem> Lines { get; private set; }
    
    public void FinalizePurchase()
    {
        // Validate business rules...
        RaiseEvent(new CartConfirmedNotification(CartId));
    }
}

public class WarehouseStockListener : INotificationHandler<CartConfirmedNotification>
{
    private readonly IRepository _stockRepo;
    private readonly IEventBus _bus;

    public async Task Handle(CartConfirmedNotification notification, CancellationToken ct)
    {
        var purchaseDetails = await _stockRepo.LoadPurchaseAsync(notification.PurchaseReference);
        
        foreach (var line in purchaseDetails.Lines)
        {
            var inventoryItem = await _stockRepo.FindBySkuAsync(line.SkuCode);
            inventoryItem.DeductStock(line.Amount);
            await _stockRepo.CommitAsync(inventoryItem);
        }
    }
}

Thẻ: ddd Domain-Driven Design C# Aggregate Root Domain Event

Đăng vào ngày 3 tháng 7 lúc 20:45