DDD - 非规范化实体作为具有 ID 的值对象?
DDD - denormalized entities as value objects with IDs?
假设我们在仓库上下文中有一个 Order。我们有一个正在获取该订单的 Client。
虽然在数据库中 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
}
然而,这里有一个令人困惑的地方:现在这看起来像一个实体(毕竟,我本质上是将来自不同上下文的 entity 非规范化为 值对象)。 "identity" 在这里没有任何意义,因为我 在检查 Client
相等性时不会关心 clientId
。
换句话说:出于所有实际目的,仓库 不 关心客户端身份。此外,仓库不能更改任何客户详细信息。但是,存在 隐含的理解,即我们正在运送给同一个客户——具有相同身份的客户。
我是否将 Client
建模为 值对象 ?这是一个常见的陷阱吗,我只是将 "Value Objects over Entities" 规则带到了绝对水平?
通常 答案是您通过 id 引用客户端,而不是缓存其属性
class Order extends AggregateRoot {
private clientId: DomainID; // or ClientID, not essential
private shipping: Shipping;
private items: Array<OrderItem>;
}
如果您需要其他客户端属性的缓存副本来维护订单本身的完整性,您只会提取它们。
Order 中的 clientId 为您提供了在需要时获取客户端数据副本所需的挂钩。挂钩通常通过拥有一个域服务来实现,该域服务了解如何从客户端 ID 中找到所需数据的副本。
另一种方法是,您还可以在订单 AR 中保留客户字段的副本。
你可能会问为什么?
因为订单(业务需求可能会有所不同)是在单个时间点发生的事情。如果订单在给定时间点发生在 CustomerId: 1
,则此时此客户有 name: John Doe
和 contactPhone: 555-abc-xyz
。这才是真正重要的(同样:业务需求可能会有所不同,但请与您的领域专家交谈)。
如果客户更改 his/her name
或 contactPhone
,您可以(也可以不 - 根据用例)更新它以供客户 PENDING 订单,但不要为 COMPLETED 订单更改它(因为更新几周或几年前发生的订单的 phone 号码没有意义)。
假设我们在仓库上下文中有一个 Order。我们有一个正在获取该订单的 Client。
虽然在数据库中 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
}
然而,这里有一个令人困惑的地方:现在这看起来像一个实体(毕竟,我本质上是将来自不同上下文的 entity 非规范化为 值对象)。 "identity" 在这里没有任何意义,因为我 在检查 Client
相等性时不会关心 clientId
。
换句话说:出于所有实际目的,仓库 不 关心客户端身份。此外,仓库不能更改任何客户详细信息。但是,存在 隐含的理解,即我们正在运送给同一个客户——具有相同身份的客户。
我是否将 Client
建模为 值对象 ?这是一个常见的陷阱吗,我只是将 "Value Objects over Entities" 规则带到了绝对水平?
通常 答案是您通过 id 引用客户端,而不是缓存其属性
class Order extends AggregateRoot {
private clientId: DomainID; // or ClientID, not essential
private shipping: Shipping;
private items: Array<OrderItem>;
}
如果您需要其他客户端属性的缓存副本来维护订单本身的完整性,您只会提取它们。
Order 中的 clientId 为您提供了在需要时获取客户端数据副本所需的挂钩。挂钩通常通过拥有一个域服务来实现,该域服务了解如何从客户端 ID 中找到所需数据的副本。
另一种方法是,您还可以在订单 AR 中保留客户字段的副本。
你可能会问为什么?
因为订单(业务需求可能会有所不同)是在单个时间点发生的事情。如果订单在给定时间点发生在 CustomerId: 1
,则此时此客户有 name: John Doe
和 contactPhone: 555-abc-xyz
。这才是真正重要的(同样:业务需求可能会有所不同,但请与您的领域专家交谈)。
如果客户更改 his/her name
或 contactPhone
,您可以(也可以不 - 根据用例)更新它以供客户 PENDING 订单,但不要为 COMPLETED 订单更改它(因为更新几周或几年前发生的订单的 phone 号码没有意义)。