这是一个实用的领域驱动设计问题:
从概念上讲,我认为我得到聚合根,直到我去定义一个。
我有一个 Employee 实体,它以聚合根的形式出现。在企业中,一些员工可能会记录与工作相关的违规行为:
员工-----*违规行为
由于并非所有员工都受此约束,我认为违规不会成为员工汇总的一部分,对吗?
因此,当我想与员工及其相关违规行为一起工作时,这是否是某些服务的两个单独的存储库交互?
最后,当我添加违规时,该方法是否适用于员工实体?谢谢您的帮助!
这是一个实用的领域驱动设计问题:
从概念上讲,我认为我得到聚合根,直到我去定义一个。
我有一个 Employee 实体,它以聚合根的形式出现。在企业中,一些员工可能会记录与工作相关的违规行为:
员工-----*违规行为
由于并非所有员工都受此约束,我认为违规不会成为员工汇总的一部分,对吗?
因此,当我想与员工及其相关违规行为一起工作时,这是否是某些服务的两个单独的存储库交互?
最后,当我添加违规时,该方法是否适用于员工实体?谢谢您的帮助!
在做了更多的研究之后,我想我已经找到了我的问题的答案。
Paul Stovell 对DDD 留言板上的一个类似问题进行了略微编辑的回复。用“客户”代替“员工”,用“订单”代替“违规”,你就明白了。
仅仅因为客户引用订单并不一定意味着订单属于客户聚合根。客户的地址可能,但订单可以是独立的(例如,您可能有一个服务来处理所有新订单,无论客户是谁。在这种情况下,必须去 Customer->Orders 是没有意义的)。
从领域的角度来看,您甚至可以质疑这些引用的有效性(客户引用了订单列表)。您实际上多久需要一次客户的所有订单?在某些系统中这是有意义的,但在其他系统中,一个客户可能会下很多订单。您可能需要某个日期范围内的客户订单,或尚未处理的客户订单,或尚未付款的订单,等等。您需要所有这些的情况可能相对不常见。但是,在处理订单时,您更有可能需要客户信息。所以在代码中,
Order.Customer.Name
是有用的,但Customer.Orders[0].LineItem.SKU
- 可能不是那么有用。当然,这完全取决于您的业务领域。
换句话说,更新客户与更新订单无关。可以想象,在我的情况下,订单或违规行为可以独立于客户/员工来处理。
如果 Violations 具有详细信息行,则 Violation 和 Violation line 将成为同一聚合的一部分,因为更改违规行可能会影响 Violation。
编辑** 我的域中的皱纹是违规行为没有任何行为。它们基本上是发生的事件的记录。尚不确定其影响。
Eric Evan 在他的书中指出,领域驱动设计:解决软件核心的复杂性,
AGGREGATE 是一组关联对象,我们将其视为一个单元,用于数据更改。
这里有两个重要的点:
我相信在您的场景中,Employee 和 Violation 不一定是一个单元,而在 Order 和 OrderItem 的示例中,它们是一个单元的一部分。
对聚合边界建模时另一件重要的事情是聚合中是否有任何不变量。不变量是应该在“整个”聚合中有效的业务规则。例如,对于 Order 和 OrderItem 示例,您可能有一个不变量,指出订单的总成本应小于预定义的金额。在这种情况下,无论何时您想将 OrderItem 添加到 Order 中,都应强制执行此不变量以确保您的 Order 有效。但是,在您的问题中,我没有看到您的实体之间有任何不变量:员工和违规。
这么简短的回答:
我相信 Employee 和 Violation 分别属于 2 个单独的集合。这些实体中的每一个也是它们自己的聚合根。所以你需要 2 个存储库:EmployeeRepository 和 ViolationRepository。
我也相信你应该有一个从 Violation 到 Employee 的单向关联。这样,每个 Violation 对象都知道它属于谁。但是,如果您想获取特定员工的所有违规列表,则可以询问 ViolationRepository:
var list = repository.FindAllViolationsByEmployee(someEmployee);
您说您有员工实体和违规行为,并且每个违规行为本身都没有任何行为。从我上面可以读到的内容,在我看来,您可能有两个聚合根:
EmployeeViolations 由相同的员工 ID 标识,它包含一系列违规对象。您可以通过这种方式将员工行为和违规行为分开,并且您不会在没有行为的情况下获得违规实体。
违规是实体还是值对象,您应该根据其属性来决定。
在这一点上,我大体上同意莫什的观点。但是,请记住从业务角度来看交易的概念。所以我实际上把“为了数据更改的目的”理解为“为了交易的目的”。
存储库是域模型的视图。在域环境中,这些“视图”真正支持或表示业务功能或能力——事务。举个例子,员工可能有一个或多个违规行为,如果是这样,是某个时间点交易的各个方面。考虑您的用例。
情景:“一名员工的行为违反了工作场所。” 这是发生的一种业务事件(即事务,或更大的、可能是分布式事务的一部分)。实际上可以从多个角度看到根受影响的域对象,这就是它令人困惑的原因。但要记住的是与业务交易相关的行为,因为您希望您的业务流程尽可能准确地对现实世界进行建模。就关系而言,就像在关系数据库中一样,您的概念域模型实际上应该已经表明了这一点(即关联性),通常可以从任一方向读取:
员工 <----commits a --------committed by ----> Violation
因此,对于这个用例,可以公平地说它是一个处理违规的事务,并且根(或“主要”实体)是一个违规。那将是您为特定业务活动或业务流程引用的聚合根。但这并不是说,对于不同的活动或流程,您不能拥有 Employee 聚合根,例如“新员工流程”。如果你小心,循环引用应该没有负面影响,或者能够以多种方式遍历你的域模型。但是,我会警告说,应该由您的业务域的控制器部分或您拥有的任何等效部分来考虑和处理对此的管理。
另外:从模式(即MVC)的角度考虑,存储库是一个视图,域对象是模型,因此还应该采用某种形式的控制器模式。通常,控制器声明存储库(聚合根的集合)的具体实现和访问。
在数据访问世界...
以 LINQ-To-SQL 为例,DataContext 将是公开 Customer 和 Order 实体视图的控制器。视图是一种非声明性的、面向框架的表类型(大致相当于存储库)。请注意,视图保留对其父控制器的引用,并且经常通过控制器来控制视图如何/何时实现。因此,控制器是您的提供者,负责映射、翻译、对象水合等。模型是您的数据 POCO。几乎是典型的 MVC 模式。
以 N/Hibernate 为例,ISession 将是通过 session.Enumerable(string query) 或 session.Get(object id) 或 session.CreateCriteria(typeof(Customer) 公开客户和订单实体视图的控制器)。列表()
在业务逻辑世界...
Customer { /*...*/ }
Employee { /*...*/ }
Repository<T> : IRepository<T>
, IEnumerable<T>
//, IQueryable<T>, IQueryProvider //optional
{ /**/ }
BusinessController {
Repository<Customer> Customers { get{ /*...*/ }} //aggregate root
Repository<Order> Orders { get{ /*...*/ }} // aggregate root
}
简而言之,让您的业务流程和事务成为指南,让您的业务基础架构随着流程/活动的实施或重构而自然发展。此外,与传统的黑盒设计相比,更喜欢可组合性。当您使用面向服务或云计算时,您会很高兴自己做到了。:)
我想知道结论会是什么?
“违规”成为一个根本实体。并且“违规”将由“员工”根实体引用。即违规存储库<->员工存储库
但是您对将违规作为根实体感到困惑,因为它没有任何行为。
但是,“行为”是作为根实体的标准吗?我不这么认为。
在这里测试理解的略微正交的问题,回到 Order...OrderItem 示例,系统中可能有一个分析模块想要直接查看 OrderItems,即获取特定产品的所有 orderItems,或所有订单项目大于一些给定的值等,是否有很多这样的用例并将“聚合根”推向极端,我们可以说 OrderItem 本身就是一个不同的聚合根吗?
这取决于。违规的任何更改/添加/删除是否会更改员工的任何部分 - 例如,您是否存储违规计数或过去 3 年内针对员工的违规计数?