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

g18*_*18c 12 entities domain-driven-design 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);
Run Code Online (Sandbox Code Playgroud)

请注意,从属标识将父标识作为组合的一部分.

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

所有内容都存在于Product聚合中,其中使用业务逻辑来强制选择和功能之间的关系不变.在我的域中,没有选择而没有相关产品的选择存在功能是没有意义的.

在查询产品以查找关联功能时,将返回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...
    }
}
Run Code Online (Sandbox Code Playgroud)

我有一个称为服务订单,这将包含一个ConfigurationLine其将引用聚合根Feature所述内Product通过聚合根FeatureId.这可能是完全不同的有界背景.

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

我的问题是:

具有父母身份的复合身份 - 好的还是坏的做法?

在其他样本DDD代码中,身份被定义为类,我还没有看到任何由本地实体id及其父身份组成的复合体.我认为这是一个不错的属性,因为我们可以随时了解到达那里的实体(总是通过聚合根)(Product - > Selection - > Feature).

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

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

蓝皮书提及的引用到聚合内的实体是可以接受的,但应该只是暂时的(一个码块内).在我的情况下,我需要存储对这些实体的引用以供将来使用,存储不是暂时的.

但是,存储此引用的需要仅用于报告和搜索目的,即使我确实想要通过根检索子实体bu导航,返回的实体也是不可变的,所以我看不到任何损害可以做或不变量破碎.

我的想法是否正确,如果是这样,为什么提到保持子实体参考是暂时的?

源代码如下:

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; 
    }
}
Run Code Online (Sandbox Code Playgroud)

Gia*_*sio 10

具有父母身份的复合身份 - 好的还是坏的做法?

当它们被正确使用时,它们是一种很好的做法:当域专家在本地识别事物时(例如"来自市场营销的约翰"),它们是正确的,否则它们是错误的.

通常,只要代码遵循专家的语言,它就是正确的.

有时,当他谈到特定的有界背景时,您会面对由专家在本地识别的全球识别实体(如"John Smith").在这些情况下,BC要求获胜.
请注意,这意味着您需要域服务来在BC之间映射标识符,否则,您只需要共享标识符.

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

如果聚合根(在您的情况下Product)要求子实体确保业务不变量,则引用必须是"长期的",至少在不变量必须保持之前.

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

值对象是不可变的,因为它们没有身份,而不是其他方式!

但是当你说:

但是,存储此引用的需要仅用于报告搜索目的

我建议你使用直接SQL查询(或使用DTO查询对象,或者你可以廉价购买的任何东西)而不是域对象.报告和搜索不会改变实体的状态,因此您不需要保留不变量.这就是CQRS的主要原理,它仅仅意味着:"只有在必须确保业务不变量时才使用域模型!使用WTF,您只需要阅读需要阅读的组件!"

额外的笔记

在查询产品的相关功能时,会返回Feature对象,但C#internal关键字用于隐藏任何可能使实体变异的方法...

如果您不需要单元测试客户端,那么在此上下文中处理修饰符的访问修饰符是一种便宜的方法,但如果您需要测试客户端代码(或引入AOP拦截器或其他任何东西),那么普通旧接口是更好的解决方案.

有人会告诉你,你正在使用"不必要的抽象",但使用语言关键字(interface)并不意味着要引入抽象!
我并不完全确定他们真正理解抽象什么,以至于他们混淆了抽象行为的工具(OO中常见的一些语言关键字).

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

sealed类混凝土?是struct小号的混凝土?没有!!!
你不能把它们扔给伤害无能的程序员!
它们与interfaces或abstract类一样抽象.

抽象是不必要的(更糟糕的是,它很危险!)如果它使代码的散文不可读,难以理解,等等.但是,相信我,它可以编码为sealed class!

...从而确保实体对调用应用程序服务是不可变的(在与域代码不同的程序集中).

恕我直言,您还应该考虑如果聚合返回的"显然不可变"的本地实体实际上可以改变其状态的一部分,那么接收它们的客户将无法知道发生了这种改变.

对我来说,我通过返回(并且还在内部使用)实际上不可变的本地实体来解决这个问题,迫使客户端只保存对聚合根(也就是主实体)的引用并在其上订阅事件.


fst*_*ijt 6

以父母身份形成的复合身份——好还是坏做法?

恕我直言,没有理由相信这是不好的做法,只要实体 id 在聚合根内是唯一的,如果实体 id 是复合的,甚至在聚合根之外是唯一的,都没有区别。人们可能会提出的唯一反对意见是,这些复合标识符与您所在领域的词汇表(“普遍语言”)中使用的标识符不同。

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

如果这些实体是不可变的,那么这些实体应该被建模为值对象。否则,通过直接引用这些实体,您将面临访问不再与给定聚合根关联或同时已更改的实体的风险。