【问题标题】:Domain Model and related data (anemic domain model)领域模型及相关数据(贫血领域模型)
【发布时间】:2016-11-22 23:48:08
【问题描述】:

我目前正在使用 Entity Framework Core 处理 ASP .NET Core 1.0。我对数据库中的数据进行了一些复杂的计算,我不确定如何使用依赖注入构建适当的架构构建贫血域模型 (http://www.martinfowler.com/bliki/AnemicDomainModel.html)

(简体)示例:

我有以下实体:

public class Project {
    public int Id {get;set;}
    public string Name {get;set;}        
}

public class TimeEntry
{
    public int Id {get;set;}
    public DateTime Date {get;set;}
    public int DurationMinutes {get;set;}
    public int ProjectId {get;set;}
    public Project Project {get;set;}        
}

public class Employee {
    public int Id {get;set;}
    public string Name {get;set;}
    public List<TimeEntry> TimeEntries {get;set;}
}

我想做一些复杂的计算来计算每月的时间表。因为我无法访问 Employee 实体中的数据库,所以我在 EmployeeService 中计算了 TimeSheet。

public class EmployeeService {
    private DbContext _db;
    public EmployeeService(DbContext db) {
        _db = db;
    }

    public List<CalculatedMonth> GetMonthlyTimeSheet(int employeeId) {
        var employee = _db.Employee.Include(x=>x.TimeEntry).ThenInclude(x=>x.Project).Single();
        var result = new List<CalculatedMonth>();

        //complex calculation using TimeEntries etc here

        return result;
    }
}

如果我想获得 TimeSheet,我需要注入 EmployeeService 并调用 GetMonthlyTimeSheet

所以 - 我最终在我的服务中使用了很多 GetThis() 和 GetThat() 方法,尽管这些方法非常适合 Employee 类本身。

我想要实现的是:

public class Employee {
    public int Id {get;set;}
    public string Name {get;set;}
    public List<TimeEntry> TimeEntries {get;set;}

    public List<CalculatedMonth> GetMonthlyTimeSheet() {            
        var result = new List<CalculatedMonth>();

        //complex calculation using TimeEntries etc here

        return result;
    }
}

public IActionResult GetTimeSheets(int employeeId) {
    var employee = _employeeRepository.Get(employeeId);
    return employee.GetTimeSheets();
}

...但为此我需要确保 TimeEntries 列表是从数据库中填充的(EF Core 不支持延迟加载)。我不想.Include(x=>y) 在每个请求中都包含所有内容,因为有时我只需要员工的姓名而不需要时间条目,这会影响应用程序的性能。

谁能指出如何正确构建这个的方向?

编辑: 一种可能性(来自第一个答案的 cmets)是:

public class Employee {
    public int Id {get;set;}
    public string Name {get;set;}
    public List<TimeEntry> TimeEntries {get;set;}

    public List<CalculatedMonth> GetMonthlyTimeSheet() { 
        if (TimeEntries == null)
          throw new PleaseIncludePropertyException(nameof(TimeEntries));

        var result = new List<CalculatedMonth>();

        //complex calculation using TimeEntries etc here

        return result;
    }
}

public class EmployeeService {
    private DbContext _db;
    public EmployeeService(DbContext db) {
        _db = db;
    }

    public Employee GetEmployeeWithoutData(int employeeId) {
        return _db.Employee.Single();
    }

    public Employee GetEmployeeWithData(int employeeId) {
        return _db.Employee.Include(x=>x.TimeEntry).ThenInclude(x=>x.Project).Single();
    }
}

public IActionResult GetTimeSheets(int employeeId) {
    var employee = _employeeService.GetEmployeeWithData(employeeId);
    return employee.GetTimeSheets();
}

【问题讨论】:

  • 避免进入贫血域模型的第一步是让你的设置器private 并创建适当的对象构造函数,并进行参数验证。此外,在使用 EF 时,如果您的 public 构造函数不是像我建议的那样无参数,则您至少需要一个 protected 构造函数。
  • 我认为考虑如何对模型进行单元测试是可行的。恕我直言,使代码可测试通常是梳理设计问题及其解决方案的绝佳方式。例如,您是否很高兴能够根据上述设计测试(单元或集成)您的EmployeeService
  • @Jetro223 正如您所介绍的那样,无论您是否愿意,您的模型贫血。

标签: c# architecture asp.net-core domain-driven-design entity-framework-core


【解决方案1】:

不要尝试用聚合解决查询问题。您的聚合旨在处理命令和保护不变量。它们围绕一组数据形成一致性边界。

Employee 对象是否负责保护员工时间表的完整性?如果不是,则此数据不属于 Employee 类。

延迟加载对于 CRUD 模型可能很好,但在我们设计聚合时通常被认为是一种反模式,因为它们应该尽可能小且具有凝聚力。

您是否根据时间表的计算结果做出业务决策?有什么不变量需要保护吗?是否根据陈旧的时间表数据做出决定是否重要?如果这些问题的答案是,那么您的计算实际上只不过是一个查询。

在服务对象中放置查询很好。这些服务对象甚至可能存在于域模型之外(例如,在应用层),但没有严格的规则可以遵循。此外,您可以选择加载一些聚合以访问处理计算所需的数据,但通常最好直接进入数据库。这样可以更好地分离读取和写入 (CQRS)。

【讨论】:

    【解决方案2】:

    如果我正确理解了您的问题,您可以使用一种技巧将服务注入您的实体以帮助它完成工作,例如:

    public class Employee()
    {
        public object GetTimeSheets(ICalculatorHelper helper)
        {
        }
    }
    

    然后在您的服务中保存员工,您将在构造函数中获取它并传递给员工类进行计算。该服务可以是 Facade,例如用于获取所有数据并执行初始化或您真正需要的任何操作。

    至于 TimeEntries,您可以使用如下函数获取它们:

    private GetTimeEntries(ICalculationHelper helper)
    {
       if (_entries == null)
       {
           _entries = helper.GetTimeEntries();
       }
       return _entries;
    }
    

    这当然取决于你的缓存策略等是否适合你。

    就我个人而言,我发现使用贫血的类很容易,并且在服务中有很多业务逻辑。我确实在对象中放了一些,例如根据 FirstName 和 LastName 计算 FullName。通常是不涉及其他服务的东西。虽然这是一个偏好问题。

    【讨论】:

    • ICalculatorHelper 对这里有什么帮助?他的问题是数据可能会也可能不会加载到对象中。
    • @HristoYankov:到目前为止,这有助于他不必担心如何获取数据,这是ICalculatorHelper(或其他命名服务)的任务,他可以保持逻辑用于模型内的计算(如果它特定于模型)或计算器助手中的计算,如果它超出给定模型的范围
    • 这是错误的。数据应该已经在类中。该类不应接收两次数据 - 一次来自属性,另一次来自某些参数注入的依赖项。
    • 所以让我总结一下:您建议该类接收对每个方法的一些依赖项,这有助于它加载其属性?我可以很容易地看到这种旋转失控并变成了成熟的意大利面条代码。
    • 谢谢 - 但为此 ICalculatorHelper 必须依赖 DbContext 才能加载相关数据。这将有助于构建更小、更专业的类,而不是巨大的 EmployeeService。但是我可以在方法中注入 DbContext(在每个方法中:-/)并从那里加载相关数据。