我有一个聚合根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
。
产品聚合中存在所有内容,其中使用业务逻辑来强制选择和功能之间的关系不变。在我的域中,如果没有选择就存在某个功能,并且没有相关产品的选择就没有意义。
在查询产品以查找关联功能时,会返回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...
}
}
我有一个名为服务订单的聚合根,它将包含一个ConfigurationLine,它将引用Feature
聚合根目录中的Product
FeatureId
。这可能是完全不同的有限背景。
由于FeatureId包含字段SelectionId
和ProductId
,因此我将了解如何通过聚合根导航到该功能。
我的问题是:
复合身份与父母的身份形成 - 好的或坏的做法?
在其他标识为类的DDD代码中,我还没有看到任何由本地实体id及其父标识组成的复合体。我认为这是一个不错的属性,因为我们可以随时了解到达那里的实体(总是通过聚合根目录)(Product - &gt; Selection - &gt; Feature)。
虽然我的代码与父组件的复合身份链是有意义的,并允许我通过根聚合导航到实体,但没有看到其他代码示例与复合材料类似地形成使我非常紧张 - 任何原因或者这是不好的做法?
对内部实体的引用 - 短暂或长期?
bluebook提及对聚合中实体的引用是可接受的,但应该只是瞬态的(在代码块内)。在我的情况下,我需要存储对这些实体的引用以供将来使用,存储不是暂时的。
然而,存储此引用的需要仅用于报告和搜索目的,即使我确实想要检索子实体bu通过root导航,返回的实体也是不可变的,所以我看不出任何伤害可以是完成或不变量。
我的想法是否正确,如果是这样,为什么提到保持子实体引用是暂时的?
源代码如下:
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;
}
}
答案 0 :(得分:10)
复合身份与父母的身份形成 - 好的或坏的做法?
当它们被正确使用时,它们是一种很好的做法:当域专家在本地识别事物时(例如“来自市场营销的约翰”),它们是正确的,否则它们是错误的。
一般来说,只要代码遵循专家的语言,就是正确的。
有时候,当他谈到特定的有界背景时,你会面对由专家在当地发现的全球识别实体(如“John Smith”)。在这些情况下, BC要求获胜 请注意,这意味着您需要域名服务才能在BC之间映射标识符,否则,您只需要shared identifiers。
对内部实体的引用 - 短暂或长期?
如果聚合根(在您的情况下为Product
)要求子实体确保业务不变量,则引用必须是“长期的”,至少在不变量必须保持之前。
此外,您正确地掌握了内部实体背后的基本原理:如果专家识别它们,它们就是实体,可变性是编程问题(不变性总是更安全)。您可以拥有不可变的实体,无论是否为本地实体,但是实体是什么使专家识别它们,而不是它们的不变性。
值对象是不可变的,因为它们没有身份,而不是其他方式!
但是当你说:
但是,存储此参考的需求仅用于报告和搜索目的
我建议您使用直接SQL查询(或使用DTO查询对象,或者您可以廉价购买的任何东西)而不是域对象。报告和搜索不会改变实体的状态,因此您不需要保留不变量。这就是CQRS的主要原理,它只是意味着:“只有在必须确保业务不变量时才使用域模型!使用WTF,您只需要阅读需要阅读的组件!”
额外备注
在查询产品的相关功能时,会返回Feature对象,但C#internal关键字用于隐藏任何可能使实体变异的方法......
如果您不需要单元测试客户端,那么在此上下文中处理修饰符的访问修饰符是一种便宜的方法,但如果您需要测试客户端代码(或引入AOP拦截器或其他任何东西),则旧的接口是一个更好的解决方案。
有人会告诉你,你正在使用“不必要的抽象”,但使用语言关键字(interface
)并不意味着要引入抽象!
我并不完全确定他们真的理解what an abstraction is,以至于他们混淆了抽象行为的工具(OO中常见的一些语言关键词)。
抽象存在于程序员的头脑中(在专家的脑海中,在DDD中),代码只是通过您使用的语言提供的结构来表达它们。
sealed
类是否具体? struct
是具体的吗?的 NO !!! 强>
你不能把它们扔给伤害无能的程序员!
它们与interface
或abstract
类一样抽象。
抽象是不必要的(更糟糕的是,这很危险!)如果它使代码的散文不可读,难以理解,等等。但是,请相信我,它可以编码为sealed class
!
...从而确保实体对调用应用程序服务是不可变的(在与域代码不同的程序集中)。
恕我直言,您还应该考虑如果聚合返回的“显然不可变”的本地实体实际上可以改变其部分状态,那么接收它们的客户将无法知道发生了这种改变。 / p>
对我来说,我通过返回(并且还在内部使用)实际上不可变的本地实体来解决这个问题,迫使客户端只保留对聚合根(也就是主实体)和subscribe events on it的引用。 / p>
答案 1 :(得分:4)
复合身份与父母的身份形成 - 好的或坏的做法?
恕我直言,没有理由相信这是不好的做法,只要实体ID在聚合根中是唯一的,如果实体id是复合的,或者甚至在聚合根之外是唯一的,它就没有区别。可能有的唯一反对意见是这些复合标识符与您域中词汇表中使用的标识符不同,无处不在 语言”。
对内部实体的引用 - 短暂或长期?
如果这些实体是不可变的,那么这些实体应该被建模为值对象。否则,通过直接引用这些实体,您将面临访问不再与给定聚合根关联的实体或同时已更改的实体的风险。
答案 2 :(得分:1)
与问题并不完全相关,但我想首先提到我没有发现界面吸引人。好像你是单向暴露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
个对象,那么这可能会使Product
对象在持久性方面非常繁重。在这种情况下,通过标识符将它们作为引用可能会更好。
除了持久层之外,代码中使用的标识符不必由子标识符和父标识符组成,因为它们已经在特定的上下文中。然而,这可以是提高数据可读性的设计决策。
组合标识的根源是SQL我认为,我已经看到了使用类来模拟这些标识的代码。我认为这更像是持久性框架或语言的限制。这只是证明手段合理性的结束。当持久性框架在某种程度上迫使您这样做时,这是可以接受的。
引用内部实体听起来像是你不应该做的事情。在Selection
,Feature
和{{1}}的示例中;没有产品进行选择是没有意义的。因此,参考该产品会更有意义。对于报告,您可能需要考虑复制数据。这实际上是NoSQL中的常用技术。特别是当实体是不可变的时,您要考虑在其他地方复制这些实体。引用一个实体将导致另一个“获取实体操作”,而数据永远不会改变,如果我这样说,这是毫无意义的。
提到父母或孩子的做法并不差。这些关系是强制执行的,这是建模的一部分,并不是没有父级的实体存在。如果要强制子实体拥有父项;要求孩子的构造函数中的父级。请不要在父级中实现create-child方法。正如我上面所说,这会使事情复杂化。我个人不会强迫父母,在创建孩子时你自己设置父母。