跳转至

领域模型

什么是领域模型

领域模型(Domain Model)是对业务领域的抽象表达,它将现实世界的业务概念、规则和流程用代码的形式表达出来。领域模型是DDD的核心,它应该:

  • 反映业务概念: 使用业务领域的语言和概念
  • 包含业务规则: 封装业务逻辑和约束
  • 独立于技术: 不依赖特定的技术实现
  • 易于理解: 代码即文档,清晰表达业务意图

领域模型的组成

一个完整的领域模型通常包含以下元素:

1. 实体 (Entity)

具有唯一标识的对象,即使属性相同,不同的实体也是不同的对象。

public class Customer : Entity<CustomerId>
{
    public string Name { get; private set; }
    public Email Email { get; private set; }
    public CustomerStatus Status { get; private set; }

    public void Activate()
    {
        if (Status == CustomerStatus.Active)
            throw new DomainException("Customer is already active");

        Status = CustomerStatus.Active;
    }
}

2. 值对象 (Value Object)

没有唯一标识,只关注属性值的对象。值对象是不可变的。

public record Email
{
    public string Value { get; }

    public Email(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Email cannot be empty");

        if (!IsValidEmail(value))
            throw new ArgumentException("Invalid email format");

        Value = value;
    }

    private static bool IsValidEmail(string email)
    {
        // 邮箱验证逻辑
        return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
    }
}

3. 聚合 (Aggregate)

一组相关对象的集合,作为数据修改的单元。聚合确保了业务不变式(Invariant)。

public class Order : AggregateRoot<OrderId>
{
    private readonly List<OrderLine> _lines = new();
    public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();

    public Money TotalAmount => Money.Sum(_lines.Select(l => l.Amount));

    public void AddLine(ProductId productId, Quantity quantity, Money unitPrice)
    {
        // 业务规则:不能添加重复产品
        if (_lines.Any(l => l.ProductId == productId))
            throw new DomainException("Product already exists in order");

        var line = new OrderLine(productId, quantity, unitPrice);
        _lines.Add(line);
    }
}

4. 领域服务 (Domain Service)

不属于任何实体或值对象的业务逻辑。

public class PriceCalculationService
{
    public Money CalculateOrderTotal(Order order, Customer customer)
    {
        var subtotal = order.TotalAmount;
        var discount = customer.VipLevel.GetDiscount();

        return subtotal * (1 - discount);
    }
}

设计原则

1. 充血模型 vs 贫血模型

❌ 贫血模型 (Anemic Model) - 应该避免

// 错误示例:只有数据,没有行为
public class Order
{
    public Guid Id { get; set; }
    public string Status { get; set; }
    public List<OrderLine> Lines { get; set; }
}

// 业务逻辑分散在服务中
public class OrderService
{
    public void Submit(Order order)
    {
        if (order.Status != "Draft")
            throw new Exception("Invalid status");
        order.Status = "Submitted";
    }
}

✅ 充血模型 (Rich Model) - 推荐

// 正确示例:封装数据和行为
public class Order : AggregateRoot<OrderId>
{
    public OrderStatus Status { get; private set; }
    private readonly List<OrderLine> _lines = new();

    public void Submit()
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException("Only draft orders can be submitted");

        Status = OrderStatus.Submitted;
        RaiseDomainEvent(new OrderSubmittedEvent(Id));
    }
}

2. 统一语言

使用业务领域的术语命名类、方法和属性。

// ✅ 好的命名
public class Invoice
{
    public void Issue() { }
    public void Cancel() { }
    public void MarkAsPaid() { }
}

// ❌ 差的命名
public class Invoice
{
    public void Process() { }  // 太模糊
    public void Delete() { }   // 不是业务术语
    public void SetStatus(int status) { }  // 暴露实现细节
}

3. 封装

隐藏内部状态,只暴露必要的行为。

public class BankAccount
{
    public Money Balance { get; private set; }

    // ✅ 提供业务方法
    public void Deposit(Money amount)
    {
        if (amount.Value <= 0)
            throw new DomainException("Deposit amount must be positive");

        Balance = Balance.Add(amount);
    }

    // ❌ 不要暴露setter
    // public void SetBalance(Money balance) { }
}

4. 不变式 (Invariant)

确保领域对象始终处于有效状态。

public class ShoppingCart
{
    private readonly List<CartItem> _items = new();
    public IReadOnlyCollection<CartItem> Items => _items.AsReadOnly();

    // 不变式:购物车最多10个商品
    private const int MaxItems = 10;

    public void AddItem(ProductId productId, int quantity)
    {
        if (_items.Count >= MaxItems)
            throw new DomainException($"Cart cannot have more than {MaxItems} items");

        // 其他业务逻辑...
    }
}

领域模型的生命周期

1. 创建

使用工厂方法或构造函数创建对象,确保对象创建时就是有效的。

public class Order
{
    // 私有构造函数
    private Order(CustomerId customerId)
    {
        Id = OrderId.New();
        CustomerId = customerId;
        Status = OrderStatus.Draft;
        CreatedAt = DateTime.UtcNow;
    }

    // 工厂方法
    public static Order CreateFor(Customer customer)
    {
        if (!customer.IsActive)
            throw new DomainException("Cannot create order for inactive customer");

        return new Order(customer.Id);
    }
}

2. 修改

通过领域方法修改状态,而不是直接修改属性。

// ✅ 通过方法修改
order.Submit();
order.AddItem(productId, quantity);
order.ApplyDiscount(discountCode);

// ❌ 不要直接修改
// order.Status = OrderStatus.Submitted;

3. 删除

根据业务需求决定是物理删除还是逻辑删除。

public class Customer
{
    public bool IsDeleted { get; private set; }

    public void Delete()
    {
        if (HasActiveOrders())
            throw new DomainException("Cannot delete customer with active orders");

        IsDeleted = true;
        RaiseDomainEvent(new CustomerDeletedEvent(Id));
    }
}

实践建议

1. 从业务出发

与领域专家沟通,理解业务概念和规则,然后再编写代码。

2. 迭代演进

领域模型不是一次性设计完成的,要根据对业务的理解不断迭代改进。

3. 单元测试

为领域模型编写充分的单元测试,确保业务规则正确实现。

[Test]
public void Submit_DraftOrder_ShouldChangeStatusToSubmitted()
{
    // Arrange
    var order = Order.CreateFor(customer);

    // Act
    order.Submit();

    // Assert
    Assert.Equal(OrderStatus.Submitted, order.Status);
}

[Test]
public void Submit_AlreadySubmittedOrder_ShouldThrowException()
{
    // Arrange
    var order = Order.CreateFor(customer);
    order.Submit();

    // Act & Assert
    Assert.Throws<DomainException>(() => order.Submit());
}

4. 保持简单

不要过度设计,从简单开始,根据需要逐步增加复杂度。

常见错误

❌ 1. 贫血模型

领域对象只有getter/setter,没有业务行为。

❌ 2. 技术泄露

在领域模型中引用基础设施细节(如数据库、HTTP等)。

❌ 3. 过度抽象

为了"灵活性"而引入不必要的抽象层。

❌ 4. 忽视不变式

允许对象进入无效状态。

下一步