domain-driven-design - 通过复合身份 DDD 导航到聚合根内的实体

标签 domain-driven-design entities software-design aggregateroot

我有一个聚合根Products其中包含实体列表 Selection ,其中又包含名为 Features 的实体列表。 .

  • 聚合根 Product有一个只有名字的身份
  • 实体 Selection具有名称标识(及其相应的产品标识)
  • 实体 Feature具有名称标识(以及相应的选择标识)

实体的身份构建如下:

var productId = new ProductId("dedisvr");
var selectionId = new SelectionId("os",productId);
var featureId = new FeatureId("windowsstd",selectionId);

请注意,从属身份将父身份的身份作为组合的一部分。

这个想法是,这将形成一个产品部件号,可以通过选择中的特定功能来识别,即 ToString()对于上述 featureId 对象将返回 dedisvr-os-windowsstd .

一切都存在于产品聚合中,其中业务逻辑用于强制选择和功能之间的关系不变。在我的领域中,没有选择而存在的功能以及没有关联产品的选择是没有意义的。

当查询产品的关联功能时,会返回Feature对象,但返回C# internal关键字用于隐藏可能改变实体的任何方法,从而确保实体对于调用应用程序服务(在与域代码不同的程序集中)是不可变的。

以上两个断言由两个函数提供:

class Product
{
    /* snip a load of other code */

    public void AddFeature(FeatureIdentity identity, string description, string specification, Prices prices)
    {
       // snip...
    }

    public IEnumerable<Feature> GetFeaturesMemberOf(SelectionIdentity identity);
    {
       // snip...
    }
}

我有一个名为 Service order 的聚合根,它将包含一个 ConfigurationLine ,它将引用 Feature Product内聚合根 FeatureId 。这可能处于完全不同的有界上下文中。

由于 FeatureId 包含字段 SelectionIdProductId我将知道如何通过聚合根导航到该功能。

我的问题是:

以 parent 身份形成的复合身份 - 好还是坏做法?

在其他将身份定义为类的示例 DDD 代码中,我还没有看到任何由本地实体 id 及其父身份构成的组合。我认为这是一个很好的属性,因为我们始终可以导航到该实体(始终通过聚合根)并了解到达该实体的路径(产品 -> 选择 -> 功能)。

虽然我的带有父级复合身份链的代码是有意义的,并且允许我通过根聚合导航到实体,但没有看到其他代码示例,其中身份与复合体类似地形成,这让我非常紧张 - 任何原因或者这是不好的做法?

对内部实体的引用 - 暂时的还是长期的?

bluebook提到对聚合中的实体的引用是可接受的,但应该只是暂时的(在代码块内)。就我而言,我需要存储对这些实体的引用以供将来使用,存储不是暂时的。

但是,存储此引用的需要仅用于报告和搜索目的,即使我确实想检索通过根导航的子实体,返回的实体也是不可变的,因此我认为不会造成任何损害完成或不变量被破坏。

我的想法是否正确?如果是,为什么提到保持子实体引用 transient ?

源代码如下:

public class ProductIdentity : IEquatable<ProductIdentity>
{
    readonly string name;

    public ProductIdentity(string name)
    {
        this.name = name;
    }

    public bool Equals(ProductIdentity other)
    {
        return this.name.Equals(other.name);
    }

    public string Name
    {
        get { return this.name; }
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public SelectionIdentity NewSelectionIdentity(string name)
    {
        return new SelectionIdentity(name, this);
    }

    public override string ToString()
    {
        return this.name;
    }
}

public class SelectionIdentity : IEquatable<SelectionIdentity>
{
    readonly string name;
    readonly ProductIdentity productIdentity;

    public SelectionIdentity(string name, ProductIdentity productIdentity)
    {
        this.productIdentity = productIdentity;
        this.name = name;
    }

    public bool Equals(SelectionIdentity other)
    {
        return (this.name == other.name) && (this.productIdentity == other.productIdentity);
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public override string ToString()
    {
        return this.productIdentity.ToString() + "-" + this.name;
    }

    public FeatureIdentity NewFeatureIdentity(string name)
    {
        return new FeatureIdentity(name, this);
    }
}

public class FeatureIdentity : IEquatable<FeatureIdentity>
{
    readonly SelectionIdentity selection;
    readonly string name;

    public FeatureIdentity(string name, SelectionIdentity selection)
    {
        this.selection = selection;
        this.name = name;
    }

    public bool BelongsTo(SelectionIdentity other)
    {
        return this.selection.Equals(other);
    }

    public bool Equals(FeatureIdentity other)
    {
        return this.selection.Equals(other.selection) && this.name == other.name;
    }

    public SelectionIdentity SelectionId
    {
        get { return this.selection; }
    }

    public string Name
    {
        get { return this.name; }
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public override string ToString()
    {
        return this.SelectionId.ToString() + "-" + this.name; 
    }
}

最佳答案

Composite identities formed with identity of parent - good or bad practice?

当它们被正确使用时,它们是一个很好的实践:当领域专家在本地识别事物时(例如“来自营销的约翰”),它们是正确的,否则它们是错误的。

一般来说,只要代码遵循专家的语言,它就是正确的。

有时,您会遇到一个全局识别的实体(例如“John Smith”),当专家谈论特定的有界上下文时,该实体在本地被识别。在这些情况下,BC 要求获胜
请注意,这意味着您将需要一个域服务来映射 BC 之间的标识符,否则,您需要的只是 shared identifiers .

References to internal entities - transient or long term?

如果聚合根(在您的情况下为Product)要求子实体确保业务不变量,则引用必须是“长期”的,至少在不变量必须成立之前是这样。

此外,您正确地掌握了内部实体背后的基本原理:如果专家识别它们,它们就是实体,可变性是编程问题(并且不变性总是更安全)。您可以拥有不可变的实体,无论是本地的还是非本地的,但使它们成为实体的是专家识别它们的事实,而不是它们的不变性。

值对象是不可变的,只是因为它们没有身份,而不是相反!

但是当你说:

However the need to store this reference is for reporting and searching purposes only

我建议您使用直接 SQL 查询(或带有 DTO 的查询对象,或任何您可以便宜获得的东西)而不是域对象。报告和搜索不会改变实体的状态,因此您不需要保留不变量。这就是 CQRS 的主要原理,简单来说就是:“只有在必须确保业务不变性的情况下才使用领域模型!对只需要读取的组件使用你喜欢的 WTF!”

额外说明

When querying the product for associated features, the Feature object is returned but the C# internal keyword is used to hide any methods that could mutate the entity...

如果您不需要对客户端进行单元测试,则在此上下文中处理修饰符的访问修饰符是一种廉价的方法,但如果您需要测试客户端代码(或引入 AOP 拦截器或其他任何内容),则普通的旧接口(interface)是更好的解决方案。

有人会告诉你,你正在使用“不必要的抽象”,但使用语言关键字( interface )并不意味着引入抽象!
我不完全确定他们是否真的理解what an abstraction is ,以至于他们将工具(面向对象中常见的一些语言关键字)与抽象行为混淆了。

抽象存在于程序员的头脑中(以及专家的头脑中,在 DDD 中),代码只是通过您使用的语言提供的构造来表达它们。

密封类是具体的吗? struct 是具体的吗? 不!!!
你不能扔它们来伤害无能的程序员!
它们与接口(interface)抽象类一样抽象。

如果抽象使代码的散文不可读、难以理解等等,那么它是不必要的(更糟糕的是,它是危险的!)。但是,相信我,它可以被编码为密封类!

... and thus ensure the entity is immutable to the calling application service (in a different assembly from domain code).

恕我直言,您还应该考虑到,如果聚合返回的“明显不可变”本地实体实际上可以更改其部分状态,则接收它们的客户端将无法知道发生了此类更改。

对我来说,我通过返回(并在内部使用)实际上不可变的本地实体来解决这个问题,强制客户端仅保留对聚合根(也称为主实体)和 subscribe events on it 的引用。 .

关于domain-driven-design - 通过复合身份 DDD 导航到聚合根内的实体,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20180404/

相关文章:

java - 类<?实现 X> : How can I define a variable that is an instance of this

asp.net - 存储库模式和 Linq to sql

c++ - 扩展包含静态函数的类

domain-driven-design - 如何在DDD上使用继承

java - "Changing"复杂的不可变对象(immutable对象)

java - 你有 Hibernate 实体的通用基类吗?

architecture - 您如何处理复合微服务请求中的验证?

c++ - C头文件可以被视为接口(interface)吗?

spring-boot - Axon Framework 中两个聚合体之间的通信

c# - 从经典和 DDD 角度来看存储库的投影