【问题标题】:DDD: What kinds of behavior should I put on a domain entity?DDD:我应该对域实体采取什么样的行为?
【发布时间】:2011-12-08 13:20:30
【问题描述】:

我的团队非常努力地坚持将领域驱动设计作为一种架构策略。但是,在大多数情况下,我们的域实体都非常贫乏。我们希望在我们的域实体上加入更多的业务/域行为。

例如,Active Record 对实体进行数据访问。我们不希望这样,因为我们很乐意使用存储库模式进行数据访问。

此外,我们将软件设计为 SOLID(鲍勃叔叔提出的五个软件设计原则)。因此,我们在设计实体时关注单一职责、开闭、liskov、接口隔离和依赖倒置对我们来说很重要。

那么,我们应该包括哪些类型的行为?我们应该远离哪些种类?

【问题讨论】:

  • 单一职责与领域驱动设计完全相反。几个月前,我们在 NYCDDD 聚会上对此进行了一次有趣的讨论......
  • 我会对那个讨论感兴趣。我觉得不是这样的

标签: domain-driven-design entity solid-principles


【解决方案1】:

我问这个问题已经快一年了,从那时起我和我的团队学到了很多东西。以下是我今天将如何回答这个问题:

域应该代表(在代码中)业务是什么或做什么(在现实生活中)。因此,域实体是在现实生活中发现的工件或参与者。那些现实生活中的人工制品和演员有什么样的行为?所有的。反过来,域实体应该对它们有什么样的行为?全部。

例如,在现实生活中,经理可以雇用一名新员工。域的表示应该包括像“经理”和“新员工”这样的实体。经理就是演员,在这里。

//newEmployee comes from somewhere else... possibly the UI
//someManagerId comes from the logged in user
var manager = _repository.Get<Manager>(someManagerId);
manager.Hire(newEmployee);

因此,经理实体在这里建模/反映了现实生活中的业务行为。另一种方法是跳过经理实体作为演员,并将他推到角落,以便繁重的“域服务”可以完成所有工作......就像这样:

//newEmployeeService comes from somewhere else... possibly injected using IOC
newEmployeeService.Create(newEmployee, someManagerId);

在贫血的域中,您可以使用这样的域服务来创建或雇用员工。它有效,但它没有表现力,并且行为不那么容易被发现。谁做了什么?为什么需要经理创建新员工?


我想当我最初问这个问题时,我想尝试开始在我的实体中包含更多行为,但我真的不知道如果不将服务注入我的实体(例如,使用构造函数注入)。从那时起,我们学到了一些新技巧,并且我们团队的实体具有超强的表现力。简而言之,这就是我们正在做的事情:

  1. 如果可能,我们会尝试使用参与者实体来表达正在执行操作的人或事物。
  2. Actor 具有表达他们可以执行的操作的方法
  3. 当需要服务时,它会作为参数注入到使用它的方法中。
  4. 我们在每个域实体的每个方法上使用 BlingBag 触发域事件,以提供可扩展性并赋予实体自我持久化的能力。

【讨论】:

  • 新员工怎么样。它是由工厂创建的还是由存储库创建的?它会通过创建引发领域事件吗?领域事件应该由存储库处理还是应该超出领域层?我可能要求太多了:D
  • 我也在尝试这样做。像 call = marketer.call(client);查询 = 营销人员.makeInquiry(客户,日期);我很好奇你是如何在 ORM 中配置你的 Manager 和数据库的设计的。管理器是单表继承吗?我正在做单表继承,但我不需要鉴别器列。基本上演员只有不同角色的相同数据。
  • +1 表示您的第 3 点(当需要服务时,将其作为参数注入到使用它的方法中)。我已经看到很多人(包括我自己)都忽略了这种微妙之处。当您考虑它时,以这种方式组织它而不是使用构造函数注入是有意义的,因为它清楚地表明了每个操作的依赖关系是什么。
【解决方案2】:

如果您必须询问应该对域实体采取什么行为,那么您可能不需要 DDD。我想在这里提供帮助,因为我在尝试将 DDD 安装到不属于它的地方时遇到了很多痛苦。

DDD 甚至domain model 是可以遵循的模式发现域复杂度太高而无法使用任何其他模式。所以只是 CRUD 不适合 DDD。根据我的理解,当您有一个包含复杂业务规则的有界上下文时,DDD 适合这种情况,这些业务规则需要在转换聚合根的状态之前运行。所以我不会在复杂的定义中包含验证。

您希望在实体中放置的行为类型与您尝试解决的业务问题密切相关。应该关注持久性(存储库等)(实际上,持久性可能在工作流或事件存储中)。

希望这会有所帮助。

【讨论】:

    【解决方案3】:

    我尝试将一些行为放入我的域、实体或值对象中。

    持久化之前的验证。 在转换到新状态之前进行验证。例如,订单聚合根实体可以在进入 Sent 状态之前验证其内部状态及其聚合子实体。 尽可能减少 get set 属性并使用值对象。首先,它使模型的行为更加丰富。实体变得更具描述性。其次,如果您必须在将地址值对象作为参数的人员实体上使用值对象方法(例如 ApplyAdress 方法),那么您很少将实体置于无效状态。

    还有什么... 情报情报。使用您的实体及其值对象来控制和限制聚合信息。像 personidentity 可以是处理一个人的唯一性的值对象。它封装了ssn、ssn算法、处理ssn上的性别校验和等。

    【讨论】:

    • 如果想就 ddd 和行为进行更深入的讨论交流,请给我发电子邮件。
    【解决方案4】:

    实体上的行为应反映业务模型。可以对该实体或由该实体做的事情是商业世界应该是可以对实体类或由实体类做的事情。例如:

    在在线购物系统中,您可以将产品添加到购物车中。所以 Cart 类应该是这样的:

    public class Cart
    {
        //...
    
        public void AddProduct(Product product)
        {
            ...code to add product to cart.
        }
    }
    

    可以说方法应该反映用例。

    【讨论】:

    • 但是如果产品代表另一个聚合根,你应该只通过 Id 引用,而不是实例。此外,作为聚合根的 Product 似乎不适合 Product.AddToCart 该建模场景
    • @Andez 为什么你说你应该通过 id 引用而不是通过引用?在 DDD 中则相反。
    • 在 Evans 蓝皮书和 Vernon 的红皮书 (p359) 中,指导不是通过引用来引用聚合,而是通过 Id 来引用它。引用:When designing Aggregates, we may desire a deep traversal through deep object graphs, but that is not the motivation of the pattern. [Evans] states that one Aggregate may hold references to the Root of other Aggregates. However, we must keep in mind that this does not place the referenced Aggregate inside the consistency boundary of the one referencing it
    • p361 弗农继续...Prefer references to external Aggregates only by their globally unique identity, not by holding a direct object reference (or pointer)
    • 这听起来像是弗农的建议,而不是埃文斯的建议。我不确定它解决了什么问题。我经常从另一个聚合中引用一个聚合,它从来没有给我带来任何问题。如果业务逻辑影响两个聚合,那么它是必要的。一些建议似乎建议您可以通过协调两个聚合的服务来避免它,但我尽量避免使用过多的服务,因为一些开发人员可以开始将所有内容都放入服务中,从而导致领域模型贫乏。
    猜你喜欢
    • 1970-01-01
    • 2010-10-23
    • 2013-08-13
    • 2010-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多