避免贫血领域模型-一个真实的例子

79

我试图理解贫血领域模型以及为什么它们被认为是反模式。

以下是一个实际的例子。

我有一个员工类,其中包含大量属性 - 姓名、性别、用户名等。

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.");
}

我没有真正遇到这种方法的太多问题,但据说这是一种贫血领域模型,所以我应该尽快离开。

但对我来说,现在并不清楚主导轮换服务中的逻辑应该放在哪里。它应该放在潜在客户身上吗?还是放在雇员身上呢?

那么所有注入的存储库等旋转服务需要什么 - 如何将它们注入到员工中,考虑到大多数情况下,当处理员工时我们都不需要这些存储库?


如果不明显地将.SelectEmployee()放入其中,那么ILead看起来是什么样子的? - Simon Buchan
好的,这种情况下的主要线索是一个网络查询,因此它将具有“评论”等属性。但我们还有电话查询、申请、报价等,它们都略有不同。ILead接口将具有像LocationOfLead、TimeOfLead等属性。 - cbp
1
所以我猜在主管身上放置.SelectEmployee()更为明显,但这并没有解决其他问题:依赖库的注入;缺乏SoC;在Lead类中拥有所有这些SelectEmployee代码的总体复杂性(实际上我们需要一个LeadBase类,以便我们不会在所有继承的主管类中重用代码),因为很多时候(例如在报告期间)我们真的不关心员工是如何被选定的。 - cbp
可能会感兴趣:https://medium.com/@wrong.about/how-to-avoid-anemic-domain-model-5e1c3e6fe4d0 - Vadim Samokhin
4个回答

59
在这种情况下,这并不构成贫血领域模型。贫血领域模型 特别关注验证和转换对象。因此,例如,如果外部函数实际上更改了员工的状态或更新了他们的详细信息,则会出现这种情况。
在这种情况下,您正在获取所有员工并根据其信息选择其中一个。拥有一个单独的对象来检查其他对象并根据其发现做出决策是可以的。但是,使用对象将对象从一种状态转换为另一种状态是不可行的。
在您的情况下,贫血领域模型的一个例子将是具有外部方法。
updateHours(Employee emp) // updates the working hours for the employee

此方法接受一个Employee对象并更新其本周工作的小时数,确保在超出一定限制时引发标志。问题在于,如果您只拥有Employee对象,则不知道如何在正确的约束条件下修改它们的工作小时数。在这种情况下,处理它的方式是将updateHours方法移动到Employee类中。这就是贫血领域模型反模式的关键所在。


但是如果员工是数据库的持久化对象,我为什么要在其中放置一个方法呢?对于DTO,您不会在其中放置方法的同样问题也是有效的。那么您将在哪里放置updateHours方法呢? - Pascal
updateHours方法属于Employee类。您应该传递任何必要的数据来更新小时数,例如完成的任务。合作者对象也可以使用,但最好不要使用服务。 - MauganRa

33

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

作为一个经验法则,如果与领域对象密切相关并且完全基于该领域对象实例定义的行为可以包含在领域对象中。否则,为了保持职责清晰,最好像你所做的那样将其放在外部的协作者/服务中。


5
确切地翻译成中文:这确实是一个带有大量内部逻辑的外部模块(例如LeadQueueManager),绝对不是贫血的领域模型。一个员工了解呼叫队列调度吗?什么也不知道 ;) - TomTom

14

所有的问题都存在于你的头脑中——考虑将轮换服务视为领域模型的一部分,问题便得到了解决。

轮换需要保留许多雇员的信息,因此它既不属于主管,也不属于任何单个员工对象。它应该成为一个独立的领域对象。

只需将“RotationService”重命名为类似“Organization.UserSupportDepartment”的名称即可明显看出。


0

如果您的领域模型仅包含角色和事物,而不是行为活动,则它是贫血的。然而,我谈论的是关于“模型”而不是“对象”的行为。我在另一个答案中讨论了它们之间的区别... https://stackoverflow.com/a/31780937/116442

从您的问题中,您违反了我的前两个领域分析建模规则:

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

我会向模型中添加一个名为“查询”的活动。有了它,模型就具有了行为,并且可以组合并作为一组对象工作,而无需外部控制器或脚本。

EnquiryHandlerModel


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接