贫血模型和充血模型

  • ~2.19K 字
  1. 1. 贫血模型 vs 充血模型

贫血模型是指一个类中只有属性或成员变量,没有方法,而充血模型指一个类中既有属性、成员变量,也有方法,这是 DDD 推荐的设计方式。

贫血模型示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 贫血模型的实体类
public class Order
{
public Guid Id { get; set; }
public decimal TotalAmount { get; set; }
public OrderStatus Status { get; set; }
// 只有属性,没有业务方法
}

public class Program
{
void Main(string args){
Order order = ctx.Orders.FindById(id);
//取消订单
if (order.Status == OrderStatus.Paid)
{
order.Status = OrderStatus.Cancelled;
order.TotalAmount = 0;
tx.Save();
}
}
}

可以看到这个类只含属性,不含逻辑方法,这样的类通常也叫POCO类。使用这个 Order 我们还需在Order类外部编写取消订单的逻辑,这需要使用者需要了解取消订单的领域逻辑,这是不符合面向对象编程的基本特征“封装性”的。我们应该把类内部的细节封装起来,对外提供领域方法,从而让类的使用者无须关心Order类需要怎么样去取消订单。

下面按照面向对象的原则设计Order类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 充血模型的实体类
public class Order
{
public Guid Id { get; private set; }
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }

// 业务逻辑封装在实体内部
public void Cancel()
{
if (Status != OrderStatus.Paid)
throw new InvalidOperationException("只能取消已支付的订单");

Status = OrderStatus.Cancelled;
TotalAmount = 0;
}

public void AddItem(OrderItem item)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("只能向草稿订单添加商品");

// 添加商品并更新总额的业务逻辑
TotalAmount += item.Price * item.Quantity;
}
}

public class Program
{
void Main(string args){
Order order = ctx.Orders.FindById(id);
//取消订单
order.Cancel();
ctx.Save();
}
}

可以看到使用者不需要知道取消订单逻辑,直接调用领域内方法就行了,这大大减少了使用者的工作量,他们也不需要了解领域知识。

这样做从代码角度来看,把本应属于Order类的行为封装到Order类中是符合“单一职责原则”的,当其他地方需要调用Order类的时候可以复用Order中方法。还可以防止对象进入非法状态。

在业务复杂的系统中,优先使用充血模型,将业务逻辑封装到领域对象内部,让代码更符合”高内聚、低耦合”的设计原则。

贫血模型 vs 充血模型

维度 贫血模型 充血模型
业务逻辑位置 Service 层或调用方 领域对象内部
封装性 差(对象只是数据容器) 好(业务逻辑内聚)
代码复用 差(逻辑分散各处) 好(逻辑封装在实体)
可维护性 差(修改逻辑需找到所有调用点) 好(修改集中在实体内部)
面向对象程度 低(过程式编程) 高(真正的 OOP)
是否符合 DDD ❌ 不推荐 ✅ 推荐