【问题标题】:Avoiding anemic domain model - a real example避免贫血的领域模型——一个真实的例子
【发布时间】:2011-02-20 17:44:53
【问题描述】:

我正在尝试了解贫血域模型以及为什么它们被认为是反模式。

这是一个真实的例子。

我有一个 Employee 类,它有很多属性 - 姓名、性别、用户名等

public class Employee
{
    public string Name { get; set; }
    public string Gender { get; set; }
    public string Username { get; set; }
    // Etc.. mostly getters and setters
}

接下来,我们有一个系统,该系统涉及在销售人员之间平均轮换来电和网站查询(称为“潜在客户”)。这个系统相当复杂,因为它涉及轮询查询、检查假期、员工偏好等。所以这个系统目前被分离成一个服务:EmployeeLeadRotationService。

public class EmployeeLeadRotationService : IEmployeeLeadRotationService
{
     private IEmployeeRepository _employeeRepository;
     // ...plus lots of other injected repositories and services

     public void SelectEmployee(ILead lead)
     {
         // Etc. lots of complex logic
     }
}

然后在我们的网站查询表的背面,我们有这样的代码:

public void SubmitForm()
{
    var lead = CreateLeadFromFormInput();

    var selectedEmployee = Kernel.Get<IEmployeeLeadRotationService>()
                                 .SelectEmployee(lead);

    Response.Write(employee.Name + " will handle your enquiry. Thanks.");
}

我并没有真正遇到这种方法的很多问题,但据说这是我应该尖叫的东西,因为它是一个贫血域模型

但对我来说,不清楚线索轮换服务中的逻辑应该去哪里。它应该领先吗?它应该进入员工吗?

轮换服务需要的所有注入的存储库等如何 - 考虑到大多数时候与员工打交道时我们不需要任何这些存储库,它们将如何注入员工?

【问题讨论】:

  • 那么ILead 看起来像什么,如果将 .SelectEmployee() 放入其中并不明显?
  • 好吧,在这种情况下,主要是网络查询,所以它会有一个 Comments 属性等。但我们也有电话查询、应用程序、报价等,它们都略有不同。 ILead 接口将具有 LocationOfLead、TimeOfLead 等属性
  • 所以我想将 .SelectEmployee() 放在首位比员工更明显,但这并没有解决其他问题:依赖存储库的注入;缺乏 SoC;在很多时候(例如在报告期间)我们在 Lead 类中包含所有这些 SelectEmployee 代码的总体复杂性(实际上我们需要一个 LeadBase 类,这样我们就不会在所有继承的潜在客户类中重用代码)真的不关心员工是如何被选中的。
  • 可能感兴趣:medium.com/@wrong.about/…

标签: anemic-domain-model


【解决方案1】:

我认为你的设计在这里很好。如您所知,贫血的域模型反模式是对避免在域对象中编码的任何行为的趋势的强烈反对。但相反,这并不意味着与域对象相关的所有 行为都必须由该对象封装。

根据经验,本质上与域对象相关联并且完全根据可以包含在域对象中的域对象实例来定义的行为。否则,为了保持职责清晰,最好像您所做的那样将其放在外部的合作者/服务中。

【讨论】:

  • 完全正确。这实际上是一个具有很多内部逻辑的外部模块(LeadQueueManager 或其他)——这绝对不是一个贫乏的域模型。员工对呼叫队列调度了解多少?什么都没有;)
【解决方案2】:

在这种情况下,这不构成贫血域模型。贫血域模型是specifically about validating and transforming the objects。因此,如果一个外部函数实际上改变了员工的状态或更新了他们的详细信息,这就是一个例子。

在这种情况下发生的情况是,您要带走所有员工,并根据他们的信息选择其中一位。有一个单独的对象来检查其他对象并根据它的发现做出决定是很好的。拥有一个用于将对象从一种状态转换到另一种状态的对象是不行的。

在您的情况下,贫血域模型的一个示例是有一个外部方法

updateHours(Employee emp) // updates the working hours for the employee

获取一个 Employee 对象并更新其一周的工作时间,确保在工作时间超过特定限制时引发标志。这样做的问题是,如果您只有 Employee 对象,那么您不知道如何在正确的约束范围内修改他们的时间。在这种情况下,处理它的方法是将 updateHours 方法移到 Employee 类中。这就是贫血领域模型反模式的症结所在。

【讨论】:

  • 但是如果 Employee 是数据库的持久对象呢?为什么要在里面放一个方法?同样的问题对于您没有将方法放入其中的 DTO 是有效的。那么你会把 updateHours 方法放在哪里呢?
  • updateHours 属于 Employee 类。您应该将更新时间所需的任何数据传递给它,例如已完成的任务。 Collaborator 对象也可以,但最好没有服务。
【解决方案3】:

这一切都在您的脑海中 - 将轮换服务视为域模型的一部分,问题就解决了。

轮换需要保存许多员工的信息,因此它既不属于领导,也不属于任何单个员工对象。它本身就应该是一个领域对象。

只需将“RotationService”重命名为“Organization.UserSupportDepartment”之类的名称即可。

【讨论】:

    【解决方案4】:

    如果您的领域模型只包含角色和事物,而不包含作为行为的活动,那么它就是贫血的。然而,我说的是关于 model 而不是 object 的行为。我在另一个答案中谈到了它们之间的区别...https://stackoverflow.com/a/31780937/116442

    从你的问题来看,你打破了我的前两个领域分析建模规则:-

    1. 被建模为(记录)活动的行为是领域模型的核心。先添加它们。
    2. 将域活动建模为类,而不是方法。

    我会在模型中添加一个活动“查询”。有了它,模型就有了行为,并且可以在没有外部控制器或脚本的情况下组合并作为一组对象工作。

    【讨论】:

      猜你喜欢
      • 2011-12-19
      • 2010-12-20
      • 2011-08-17
      • 2014-01-05
      • 1970-01-01
      • 2010-12-26
      • 2012-02-04
      • 2011-10-13
      • 2020-01-29
      相关资源
      最近更新 更多