假设我们在仓库上下文中有一个订单。我们有一个客户正在获得该订单。
虽然在数据库中Client
有一个ID和一堆参数,但我在我的订单中将其建模为一个值对象:
class Order extends AggregateRoot {
private client: Client;
private shipping: Shipping;
private items: Array<OrderItem>;
}
class Client extends ValueObject {
public readonly name: string;
public readonly contactPhone: string;
public readonly contactEmail: string;
}
我将其建模为值对象的原因很简单 - 我不认为仓库关心客户是谁。数据主要用于预订快递员,在这种情况下,客户无法真正更改其姓名或联系方式 - 这将需要调整快递员预订(此时,不妨将其视为完全不同的客户)。
现在我想知道客户端 ID(用于某些分析或可追溯性)实际上会很好。容易:
class Client extends ValueObject {
public readonly name: string;
public readonly contactPhone: string;
public readonly contactEmail: string;
public readonly clientId: DomainID; // or ClientID, not essential
}
但是,这里有一个令人困惑的点:现在这看起来像一个实体(毕竟,我本质上是将来自不同上下文的实体非规范化为值对象)。"身份"在这里没有真正的意义,因为我在检查Client
平等时不会关心clientId
。
换句话说:出于所有实际目的,仓库并不关心客户身份。此外,仓库不能更改任何客户详细信息。但是,存在一种隐含的理解,即我们正在运送到同一个客户 - 具有相同身份的客户。
我可以将Client
建模为值对象吗?这是一个常见的陷阱,我只是将"价值对象高于实体"规则提升到绝对水平?
通常的答案是按 id 引用客户端,而不是缓存其属性
class Order extends AggregateRoot {
private clientId: DomainID; // or ClientID, not essential
private shipping: Shipping;
private items: Array<OrderItem>;
}
仅当需要其他客户端属性的缓存副本来维护订单本身的完整性时,才会拉入它们。
顺序中的 clientId 为您提供了在需要时获取客户端数据副本所需的钩子。 钩子通常可以通过拥有一个域服务来实现,该服务了解如何从客户端 ID 中查找所需数据的副本。
另一种方法是,您还可以在 Order AR 中保留客户端字段的副本。
你可能会问为什么?
因为顺序(但是业务需求可能会有所不同)是在单个时间点上发生的事情。如果订单在给定时间点碰巧CustomerId: 1
,则在此时间点,该客户已name: John Doe
并contactPhone: 555-abc-xyz
。这才是真正重要的(同样:业务需求可能会有所不同,但请与您的领域专家交谈)。
如果客户更改他/她的name
或contactPhone
您可以(也可能不会 - 取决于用例)为客户的待处理订单更新它,但不要为已完成订单更改它(因为更新几周或几年前发生的订单的电话号码是没有意义的)。