设计微服务域模型

小窍门

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可以在 .NET Docs 上获取,也可以下载免费的 PDF 以供离线阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

为每个业务微服务或边界上下文定义一个丰富的域模型。

目标是为每个业务微服务或边界上下文(BC)创建一个统一域模型。 但是,请记住,BC 或业务微服务有时可能由共享单个域模型的多个物理服务组成。 域模型必须捕获它所表示的单个边界上下文或业务微服务的规则、行为、业务语言和约束。

域实体模式

实体表示领域对象,主要是由它们的标识、连续性和随着时间推移的持久性定义,而不仅仅是由构成它们的属性定义。 正如 Eric Evans 所说,“主要由其标识定义的对象称为实体。实体在域模型中非常重要,因为它们是模型的基础。 因此,应仔细识别和设计它们。

实体的标识可以跨多个微服务或绑定上下文。

同一标识(即相同的 Id 值,尽管可能不是相同的域实体)可以跨多个边界上下文或微服务建模。 但是,这并不意味着同一实体具有相同的属性和逻辑将在多个边界上下文中实现。 相反,每个限界上下文中的实体将其属性和行为限制在该限界上下文领域中所需的范围。

例如,买方实体可能具有在个人简介或身份微服务中的用户实体定义的大部分属性,包括身份。 但是,订购微服务中的买家实体可能具有较少的属性,因为只有某些买家数据与订单过程相关。 每个微服务或限定上下文的上下文会影响其领域模型。

除了实现数据属性之外,域实体还必须实现行为。

DDD 中的域实体必须实现与实体数据相关的域逻辑或行为(内存中访问的对象)。 例如,作为订单实体类的一部分,你必须将业务逻辑和操作作为方法来实现以执行任务,例如添加订单项、数据验证和总计计算。 实体的方法处理实体的不变性和业务规则,而不是将这些规则分散在应用程序层。

图 7-8 显示了一个域实体,该实体不仅实现数据属性,还实现具有相关域逻辑的作或方法。

显示域实体模式的图示。

图 7-8. 实现数据加行为的域实体设计示例

域模型实体通过方法实现行为,即不是“贫血”模型。 当然,实体有时可能不会在实体类中实现任何逻辑。 如果子实体没有任何特殊逻辑,则聚合中的子实体可能会发生这种情况,因为大部分逻辑是在聚合根中定义的。 如果你有一个复杂的微服务,其中逻辑是在服务类中实现的而不是在领域实体中实现的,那么你可能会陷入一种贫血领域模型,这会在接下来的部分进行解释。

丰富域模型与贫乏域模型

在帖子AnemicDomainModel中,Martin Fowler 将贫血领域模型描述为:

贫乏域模型的基本症状是,乍一看上去像是真实存在的。 有对象,许多以域空间中的名词命名,这些对象与真实域模型具有的丰富关系和结构相连。 但当你观察它的行为时,问题来了,你发现这些对象几乎没有任何行为,完全就是一些 getter 和 setter 而已。

当然,当使用贫血域模型时,这些数据模型将通过一个服务对象集合来使用,这些服务对象传统上被称为业务层,并负责捕获所有域或业务逻辑。 业务层位于数据模型之上,并简单地将数据模型用作数据。

贫血领域模型只是一种过程型设计。 贫血模型对象不是实际对象,因为它们缺少行为(方法)。 它们只保存数据属性,因此它不是面向对象的设计。 通过将所有行为放入服务对象(业务层),你基本上最终会得到 意大利面代码事务脚本,因此你失去了域模型提供的优势。

不管怎样,如果微服务或界限上下文非常简单(CRUD 服务),只包含数据属性的实体对象形式的贫血领域模型可能已经足够好,可能不值得实现更复杂的 DDD 模式。 在这种情况下,它就是一个持久性模型,因为你特意创建了一个仅包含用于 CRUD 的数据的实体。

这就是为什么微服务体系结构特别适用于多体系结构方法(具体取决于每个绑定上下文)。 例如,在 eShopOnContainers 中,排序微服务实现 DDD 模式,但目录微服务(一个简单的 CRUD 服务)则不会实现。

有人说,贫血领域模型是一种反模式。 这确实取决于要实现的内容。 创建的微服务如果足够简单(例如 CRUD 服务),那么遵循贫血领域模型并不是一种反模式。 但是,如果需要解决包含大量不断变化的业务规则的微服务域的复杂性,那么贫乏域模型可能是该微服务或绑定上下文的反模式。 在这种情况下,将其设计为包含数据加行为的实体的丰富模型,以及实现其他 DDD 模式(聚合、值对象等)对于此类微服务的长期成功可能具有巨大的好处。

其他资源

值对象模式

正如埃里克·埃文斯指出的,“许多对象没有概念性标识。 这些对象描述事物的某些特征。

实体需要标识,但系统中有许多对象不需要标识,例如值对象模式。 值对象是一个没有描述域方面的概念标识的对象。 这些对象实例化后可表示设计元素,你只会暂时关注它们。 你关心他们 是什么 ,而不是他们 是谁 。 示例包括数字和字符串,但也可以是更高级别的概念,如属性组。

微服务中的实体可能不是另一个微服务中的实体,因为在第二种情况下,绑定上下文可能有不同的含义。 例如,电子商务应用程序中的地址可能根本不具有标识,因为它可能只表示个人或公司的客户个人资料的一组属性。 在这种情况下,地址应分类为值对象。 但是,在电力公用事业公司的应用程序中,客户地址对于业务领域可能很重要。 因此,地址必须具有标识,以便计费系统可以直接链接到该地址。 在这种情况下,地址应分类为域实体。

具有名称和姓氏的人通常是实体,因为一个人有身份,即使名称和姓氏与另一组值相吻合,例如,这些名称也引用了另一个人。

值对象很难在关系数据库和 ORM(如 Entity Framework(EF)中管理,而在面向文档的数据库中,它们更易于实现和使用。

EF Core 2.0 及更高版本包括 “拥有的实体 ”功能,可更轻松地处理值对象,稍后我们将详细介绍这一点。

其他资源

聚合模式

域模型包含不同数据实体和进程的群集,这些实体和进程可以控制功能的重要区域,例如订单履行或库存。 更细粒度的 DDD 单元是聚合,它描述一个群集或一组实体和行为,这些实体和行为可以被视为一个凝聚力单元。

通常基于所需事务来定义聚合。 经典示例是包含订单项列表的订单。 订单项通常是一个实体。 但它将是订单聚合中的子实体,它还将包含订单实体作为其根实体,通常称为聚合根。

识别聚合可能很难。 聚合是一组必须一致的对象,但不能只选取一组对象并将其标记为聚合。 必须从域概念开始,并考虑在与该概念相关的最常见事务中使用的实体。 需要事务一致性的实体是构成聚合的实体。 考虑事务操作可能是识别聚合的最佳方式。

聚合根或根实体模式

聚合由至少一个实体组成:聚合根,也称为根实体或主实体。 此外,它可以具有多个子实体和值对象,所有实体和对象协同工作以实现所需的行为和事务。

聚合根的目的是确保聚合的一致性;它应该是通过聚合根类中的方法或作更新聚合的唯一入口点。 应仅通过聚合根对聚合中的实体进行更改。 它是聚合的一致性守护者,它会考虑到可能需要在聚合中遵守的所有不变量和一致性规则。 如果单独更改子实体或值对象,聚合根不能确保聚合处于有效状态。 这就像一张桌脚松动了的桌子。 维护一致性是聚合根的主要用途。

在图 7-9 中,可以看到示例聚合,如购买者聚合,其中包含单个实体(聚合根买家)。 顺序聚合包含多个实体和一个值对象。

买家聚合与订单聚合对比图。

图 7-9. 包含多个或单个实体的聚合示例

DDD 域模型由聚合组成,聚合只能包含一个实体或多个实体,还可以包含值对象。 请注意,视你的域而定,Buyer 聚合可能会有其他子实体,就像在 eShopOnContainers 参考应用程序的订购微服务中那样。 图 7-9 仅列举了买家具有单个实体的情况,作为仅包含聚合根的聚合示例。

为了保持聚合的分离并保持它们之间的明确边界,DDD 域模型中的一种良好做法是禁止在聚合之间直接导航,并且只有外键 (FK) 字段,如 eShopOnContainers 中的 “订购微服务域模型 ”中实现的。 Order 实体只包含一个针对买家的外键字段,但不包括 EF Core 导航属性,如以下代码所示:

public class Order : Entity, IAggregateRoot
{
    private DateTime _orderDate;
    public Address Address { get; private set; }
    private int? _buyerId; // FK pointing to a different aggregate root
    public OrderStatus OrderStatus { get; private set; }
    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
    // ... Additional code
}

识别和使用聚合需要研究和经验。 有关详细信息,请参阅以下其他资源列表。

其他资源