15 KiB
15 KiB
name, description, compatibility, metadata
| name | description | compatibility | metadata | ||||
|---|---|---|---|---|---|---|---|
| domain-driven-design | DDD patterns cho complex business logic. Use for Aggregates, Value Objects, Entities, Domain Events, và Rich Domain Model. | .NET 8+, EF Core 8+ |
|
Domain-Driven Design Patterns / Mẫu DDD
DDD patterns cho GoodGo microservices với complex business logic.
When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Modeling complex business domains / Mô hình hóa domain phức tạp
- Designing aggregates and entities / Thiết kế aggregates và entities
- Implementing business rules in domain / Triển khai business rules trong domain
- Creating value objects / Tạo value objects
- Raising domain events / Raise domain events
Core Concepts / Khái Niệm Cốt Lõi
DDD Building Blocks / Các Khối Xây Dựng DDD
┌─────────────────────────────────────────────────────────────┐
│ DOMAIN MODEL LAYER │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AGGREGATE ROOT │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Entity │ │ Value Object │ │ │
│ │ │ (Identity) │ │ (No Identity) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ • Business Rules / Quy tắc nghiệp vụ │ │
│ │ • Domain Events / Domain Events │ │
│ │ • Invariants / Bất biến │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Domain Service │ │ Domain Events │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Entity vs Value Object / Entity vs Value Object
| Aspect | Entity | Value Object |
|---|---|---|
| Identity | Has unique ID | No identity |
| Equality | By ID | By all properties |
| Mutability | Mutable (via methods) | Immutable |
| Lifecycle | Independent | Belongs to Entity |
| Example | Order, User | Address, Money |
Aggregate Rules / Quy Tắc Aggregate
- One repository per aggregate root / Một repository cho mỗi aggregate root
- Reference only by ID / Chỉ tham chiếu qua ID
- Atomic transaction boundary / Ranh giới transaction atomic
- Consistency within aggregate / Nhất quán trong aggregate
Key Patterns / Mẫu Chính
Entity Base Class
/// <summary>
/// EN: Base class for all entities.
/// VI: Base class cho tất cả entities.
/// </summary>
public abstract class Entity
{
private int? _requestedHashCode;
private List<IDomainEvent>? _domainEvents;
public virtual Guid Id { get; protected set; }
public IReadOnlyCollection<IDomainEvent> DomainEvents
=> _domainEvents?.AsReadOnly() ?? Array.Empty<IDomainEvent>().AsReadOnly();
public void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents ??= new List<IDomainEvent>();
_domainEvents.Add(eventItem);
}
public void RemoveDomainEvent(IDomainEvent eventItem)
{
_domainEvents?.Remove(eventItem);
}
public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
public bool IsTransient()
{
return Id == default;
}
public override bool Equals(object? obj)
{
if (obj is not Entity other)
return false;
if (ReferenceEquals(this, other))
return true;
if (GetType() != other.GetType())
return false;
if (IsTransient() || other.IsTransient())
return false;
return Id.Equals(other.Id);
}
public override int GetHashCode()
{
if (!IsTransient())
{
_requestedHashCode ??= Id.GetHashCode() ^ 31;
return _requestedHashCode.Value;
}
return base.GetHashCode();
}
public static bool operator ==(Entity? left, Entity? right)
{
return left?.Equals(right) ?? right is null;
}
public static bool operator !=(Entity? left, Entity? right)
{
return !(left == right);
}
}
Aggregate Root
/// <summary>
/// EN: Marker interface for aggregate roots.
/// VI: Interface đánh dấu aggregate roots.
/// </summary>
public interface IAggregateRoot { }
/// <summary>
/// EN: Order aggregate root with business rules.
/// VI: Order aggregate root với business rules.
/// </summary>
public class Order : Entity, IAggregateRoot
{
private readonly List<OrderItem> _orderItems = new();
public string UserId { get; private set; }
public Address ShippingAddress { get; private set; }
public OrderStatus Status { get; private set; }
public decimal TotalAmount { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? SubmittedAt { get; private set; }
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
// EN: Required by EF Core
private Order() { }
public Order(string userId, Address shippingAddress)
{
Id = Guid.NewGuid();
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
Status = OrderStatus.Draft;
CreatedAt = DateTime.UtcNow;
TotalAmount = 0;
// EN: Raise domain event
// VI: Raise domain event
AddDomainEvent(new OrderCreatedDomainEvent(Id, userId));
}
public void AddItem(Guid productId, int quantity, decimal unitPrice)
{
// EN: Business rule: Can only add items to draft orders
// VI: Quy tắc: Chỉ thêm items vào orders draft
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot add items to non-draft order");
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive", nameof(quantity));
if (unitPrice < 0)
throw new ArgumentException("Price cannot be negative", nameof(unitPrice));
var existingItem = _orderItems.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
_orderItems.Add(new OrderItem(productId, quantity, unitPrice));
}
RecalculateTotal();
}
public void RemoveItem(Guid productId)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot remove items from non-draft order");
var item = _orderItems.FirstOrDefault(i => i.ProductId == productId);
if (item != null)
{
_orderItems.Remove(item);
RecalculateTotal();
}
}
public void Submit()
{
// EN: Business rule: Cannot submit empty order
// VI: Quy tắc: Không thể submit order trống
if (!_orderItems.Any())
throw new DomainException("Cannot submit empty order");
if (Status != OrderStatus.Draft)
throw new DomainException($"Cannot submit order in {Status} status");
Status = OrderStatus.Submitted;
SubmittedAt = DateTime.UtcNow;
AddDomainEvent(new OrderSubmittedDomainEvent(Id, UserId, TotalAmount));
}
public void Cancel(string reason)
{
if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered)
throw new DomainException("Cannot cancel shipped or delivered order");
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledDomainEvent(Id, reason));
}
private void RecalculateTotal()
{
TotalAmount = _orderItems.Sum(i => i.Quantity * i.UnitPrice);
}
}
Value Object
/// <summary>
/// EN: Base class for value objects.
/// VI: Base class cho value objects.
/// </summary>
public abstract class ValueObject
{
protected abstract IEnumerable<object?> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj is null || obj.GetType() != GetType())
return false;
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
}
public static bool operator ==(ValueObject? left, ValueObject? right)
{
return left?.Equals(right) ?? right is null;
}
public static bool operator !=(ValueObject? left, ValueObject? right)
{
return !(left == right);
}
}
/// <summary>
/// EN: Address value object.
/// VI: Value object địa chỉ.
/// </summary>
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string State { get; }
public string PostalCode { get; }
public string Country { get; }
public Address(string street, string city, string state, string postalCode, string country)
{
Street = street ?? throw new ArgumentNullException(nameof(street));
City = city ?? throw new ArgumentNullException(nameof(city));
State = state ?? throw new ArgumentNullException(nameof(state));
PostalCode = postalCode ?? throw new ArgumentNullException(nameof(postalCode));
Country = country ?? throw new ArgumentNullException(nameof(country));
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return State;
yield return PostalCode;
yield return Country;
}
}
/// <summary>
/// EN: Money value object with currency.
/// VI: Value object tiền tệ.
/// </summary>
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");
Amount = amount;
Currency = currency?.ToUpperInvariant()
?? throw new ArgumentNullException(nameof(currency));
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}
Domain Events
/// <summary>
/// EN: Domain event interface.
/// VI: Interface domain event.
/// </summary>
public interface IDomainEvent : INotification
{
Guid Id { get; }
DateTime OccurredOn { get; }
}
/// <summary>
/// EN: Domain event when order is submitted.
/// VI: Domain event khi order được submit.
/// </summary>
public record OrderSubmittedDomainEvent : IDomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
public Guid OrderId { get; init; }
public string UserId { get; init; }
public decimal TotalAmount { get; init; }
public OrderSubmittedDomainEvent(Guid orderId, string userId, decimal totalAmount)
{
OrderId = orderId;
UserId = userId;
TotalAmount = totalAmount;
}
}
Common Mistakes / Lỗi Thường Gặp
1. Anemic Domain Model
// ❌ BAD: Anemic model with no behavior
public class Order
{
public Guid Id { get; set; }
public string Status { get; set; }
public List<OrderItem> Items { get; set; }
}
// ✅ GOOD: Rich domain model with behavior
public class Order
{
private readonly List<OrderItem> _items = new();
public OrderStatus Status { get; private set; }
public void AddItem(Guid productId, int quantity, decimal price)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot modify non-draft order");
// ...
}
}
2. Direct Property Modification
// ❌ BAD: Direct modification bypasses rules
order.Status = OrderStatus.Submitted;
// ✅ GOOD: Use domain methods
order.Submit();
3. Logic in Application Layer
// ❌ BAD: Business logic in handler
public async Task Handle(CreateOrderCommand cmd)
{
if (cmd.Items.Count == 0)
throw new Exception("Empty order");
// ...
}
// ✅ GOOD: Business logic in domain
public void Submit()
{
if (!_orderItems.Any())
throw new DomainException("Cannot submit empty order");
}
Quick Reference / Tham Chiếu Nhanh
Aggregate Design Guidelines
| Guideline | Description |
|---|---|
| Small aggregates | Keep aggregates focused |
| Reference by ID | 외부 aggregates chỉ tham chiếu qua ID |
| Eventual consistency | Between aggregates |
| Immediate consistency | Within aggregate |
When to Use Each Pattern
| Pattern | Use When |
|---|---|
| Entity | Has identity, lifecycle |
| Value Object | No identity, immutable |
| Aggregate | Group of related entities |
| Domain Event | Side effects needed |
| Domain Service | Logic doesn't fit entity |
Resources / Tài Nguyên
- Detailed Examples - Full code examples
- Repository Pattern - Data access
- CQRS MediatR - Command handlers
- Testing Patterns - Domain testing