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

Posted

技术标签:

【中文标题】DDD 通过复合身份导航到聚合根内的实体【英文标题】:DDD navigation to entities inside aggregate root via composite identity 【发布时间】:2013-12-09 10:21:08 【问题描述】:

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

聚合根 Product 的标识只是名称 实体Selection 具有名称标识(及其对应的产品标识) 实体Feature 具有名称标识(也是对应的选择标识)

实体的身份构建如下:

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

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

这个想法是,这将形成一个产品部件号,可以通过选择中的特定功能来识别,即上述 featureId 对象的ToString() 将返回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...
    

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

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

我的问题是:

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

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

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

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

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

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

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

源码如下:

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; 
    

【问题讨论】:

当对内部实体对象的引用传递到聚合的out 时,对聚合内的实体的引用应该是暂时的。它不适用于实际聚合。 谢谢@Eben,为什么对聚合根目录中的实体是瞬态的?这与将从存储库中提取的聚合的引用标识有何不同?随意在下面回答:) 我希望我的理解正确:) --- 但是说Truck 包含Tyre 实例的集合。例如,如果 ConditionMonitorTruck 对象传递了 Tyre 实例,那么 that Tyre 实例应该是瞬态的。任何Truck 永远拥有自己的实体都很好。 【参考方案1】:

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

如果使用得当,它们是一种很好的做法:当领域专家在本地识别事物时(例如“来自营销部的约翰”),它们是正确的,否则它们就是错误的。

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

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

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

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

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

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

但是当你说:

但存储此参考的需要仅用于报告搜索目的

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

补充说明

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

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

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

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

sealed 类是具体的吗? structs 是具体的吗? 不!!! 你不能扔它们来伤害不称职的程序员! 它们与interfaces 或abstract 类一样抽象。

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

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

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

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

【讨论】:

【参考方案2】:

与问题不完全相关,但我想首先提到我不觉得界面吸引人。似乎您正在单向公开 Feature 类。要么暴露,要么不暴露。 我不是 C# 开发人员,所以如果我犯了任何语法错误,请不要介意。 为了说明我的意思:

这会暴露特征的属性。当这些属性发生变化时,无论哪种方式,这个接口也需要改变。

public void AddFeature(FeatureIdentity identity, string description,
                       string specification, Prices prices)

您可能需要考虑接受 Feature 对象作为参数:

public void AddFeature(Feature feature)

这比 IMO 更干净。

关于主题;你的问题让我想起了 NoSQL 设计。我对这些有点熟悉,所以我可能会有偏见,可能会错过重点。

可以通过多种方式将子标识符与父标识符组合在一起,并且可能是也可能不是坏习惯。考虑如何访问您的实体。如果您从父实体访问子实体,则聚合是有意义的。如果子实体也可能是根实体,那么您需要引用它们。你已经在你的问题中做出了这个决定。

如果一个功能没有选择就存在,而选择没有关联产品就没有意义。

你的Product 类有一个包含Selection 对象的集合是有道理的。 Selection 类将有一个包含 Feature 对象的集合。请注意,这可能会使Product 对象非常重,就持久性而言,如果它有很多Selection 对象,而其中可能有很多Feature 对象。在这种情况下,将它们作为标识符的引用可能会更好。

在您的代码中使用的标识符,除了持久层之外,不必由子标识符和父标识符组成,因为它们已经在特定的上下文中。然而,这可能是提高数据可读性的设计决策。

我认为组合身份源于 SQL,我已经看到使用类对这些身份进行建模的代码。我喜欢认为这更多是持久性框架或语言的限制。这只是证明手段合理的目的。当持久性框架在某种程度上迫使您这样做时,这是可以接受的。

引用内部实体听起来像是不应该做的事情。在ProductSelectionFeature的例子中;没有产品的选择是没有意义的。因此,提及产品会更有意义。对于报告,您可能需要考虑复制数据。这实际上是 NoSQL 中的一种常用技术。特别是当实体是不可变的时,您需要考虑在其他地方复制这些实体。引用一个实体将导致另一个“获取实体操作”,而数据永远不会改变,如果我可以这么说,这是毫无意义的。

提及父母或孩子根本不是坏习惯。这些关系是强制执行的,这是建模的一部分,并不是没有父实体存在。如果你想强制一个子实体有一个父实体;在孩子的构造函数中需要父母。请不要在父级中实现 create-child 方法。正如我上面所说,这会使事情复杂化。我个人不会强迫有父母,在创建孩子时你自己设置父母。

【讨论】:

【参考方案3】:

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

恕我直言,没有理由相信这是不好的做法,只要实体 id 在聚合根中是唯一的,那么实体 id 是复合的,甚至在聚合根之外是唯一的都没有区别。唯一的反对意见是,这些复合标识符不同于您的领域词汇表中使用的标识符,“无处不在” 语言”。

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

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

【讨论】:

与无处不在的语言保持一致的好点。关于不变性,我从 Vaughn Veron 的书和演示 c# 代码中读到他对 Tasks 做了类似的事情,它们是聚合内的实体,具有自己的可变属性,这些属性被标记为内部。对于外部应用程序代码,这些属性是隐藏的,因此它们对于除域模型之外的所有人都是不可变的,这些是具有 guid 身份的实体,而不是值对象

以上是关于DDD 通过复合身份导航到聚合根内的实体的主要内容,如果未能解决你的问题,请参考以下文章

DDD中聚合聚合根的含义以及作用

DDD开发实践流程

DDD专栏5:深入DDD的核心:领域与限界上下文

聚合根中覆盖的实体如何保存在 DDD 中?

DDD—聚合和聚合根

DDD 在一个聚合中的实体之间以一对多而不是一对多的聚合边界