Migrate
This commit is contained in:
480
microservices/.agent/skills/domain-driven-design/SKILL.md
Normal file
480
microservices/.agent/skills/domain-driven-design/SKILL.md
Normal file
@@ -0,0 +1,480 @@
|
||||
---
|
||||
name: domain-driven-design
|
||||
description: DDD patterns cho complex business logic. Use for Aggregates, Value Objects, Entities, Domain Events, và Rich Domain Model.
|
||||
compatibility: ".NET 8+, EF Core 8+"
|
||||
metadata:
|
||||
author: Velik Ho
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
1. **One repository per aggregate root** / Một repository cho mỗi aggregate root
|
||||
2. **Reference only by ID** / Chỉ tham chiếu qua ID
|
||||
3. **Atomic transaction boundary** / Ranh giới transaction atomic
|
||||
4. **Consistency within aggregate** / Nhất quán trong aggregate
|
||||
|
||||
## Key Patterns / Mẫu Chính
|
||||
|
||||
### Entity Base Class
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
/// <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
|
||||
|
||||
```csharp
|
||||
// ❌ 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
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Direct modification bypasses rules
|
||||
order.Status = OrderStatus.Submitted;
|
||||
|
||||
// ✅ GOOD: Use domain methods
|
||||
order.Submit();
|
||||
```
|
||||
|
||||
### 3. Logic in Application Layer
|
||||
|
||||
```csharp
|
||||
// ❌ 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](./references/REFERENCE.md) - Full code examples
|
||||
- [Repository Pattern](../repository-pattern/SKILL.md) - Data access
|
||||
- [CQRS MediatR](../cqrs-mediatr/SKILL.md) - Command handlers
|
||||
- [Testing Patterns](../testing-patterns/SKILL.md) - Domain testing
|
||||
Reference in New Issue
Block a user