繁体   English   中英

DDD 和实体框架类

[英]DDD and Entity Framework classes

我已经阅读了很多关于 DDD 的文章并理解,我应该在基础设施级别使用我的域模型类,因此,我应该使用与实体框架基础设施相同的类并使用它们来生成表(代码优先方法)等。但是我的领域模型可能与关系数据库模型完全不同。

为什么我不能再创建一个模型、基础结构模型来创建关系数据库模型并且不将域模型与 EF 类混合?

考虑这个简单的例子:

领域模型

public class Customer
{
    public Customer(IRegistrar registrar)
    {
        this.registrar = registrar;
    }

    public int Age
    {
        get
        {
            // Just for this example. This will not work for all locals etc but beyond the point here.
            var today = DateTime.Today;
            return today.Year - this.DateOfBirth.Year;
        }
    }

    public DateTime DateOfBirth { get; set; }

    public int Register()
    {
        if (this.Age < 18)
        {
            throw new InvalidOperationException("You must be at least 18 years old");
        }

        int id = this.registrar.Register(this);

        return id;
    }
}

public interface IRegistrar 
{
    public int Register(Customer customer);
}

很多人在没有域模型时会在 MVC 控制器中执行此操作:

public ActionResult Search(Customer customer)
{
    var today = DateTime.Today;
    var age = today.Year - this.DateOfBirth.Year;
    if (age < 18)
    {
        // Return an error page or the same page but with error etc.
    }

    // All is good
    int id = this.registrar.Register(customer);

    // The rest of code
}

有几个问题:

  1. 如果开发人员在致电registrar之前忘记检查年龄怎么办? 很多人会说,那是一个糟糕的开发人员。 无论如何,这种类型的代码很容易出现错误。

  2. 该产品运行良好,因此 CFO 决定开放 API,因为有许多开发人员正在为客户注册制作出色的 UI 界面,并且他们想使用我们的 API。 因此,开发人员继续创建一个 WCF 服务,如下所示:

     public int Register(Customer customer) { var today = DateTime.Today; var age = today.Year - this.DateOfBirth.Year; if (age < 18) { // Return a SOAP fault or some other error } int id = this.registrar.Register(customer); // The rest of code }
  3. 现在开发人员可以忘记在 2 个不同的地方检查年龄。

  4. 代码也在 2 个不同的地方。 如果有错误,我们需要记住在 2 个不同的地方修复它。
  5. 如果公司在法定年龄为 21 岁的地方开始运营,我们需要找到所有地方并添加此规则。
  6. 如果我们正在与 BA 讨论规则,那么我们需要查看所有应用程序并找到规则。

在上面的例子中,我们只有一个规则:年龄必须大于 18。如果我们有更多的规则和更多的类怎么办? 你可以看到这会去哪里。


EF模型

您的 EF 模型可能是这样的:

public class Customer
{
    public int Id { get; set; }
    public DateTime DateOfBirth { get; set; }  

    // It may have a foreign key etc.    
}

应用层模型

你的 MVC 视图模型可能是这样的:

public class Customer
{
    // Or instead of Domain.Customer, it may be a CustomerDto which is used
    // to transfer data from one layer or tier to another.
    // But you get the point.
    public Customer(Domain.Customer customer)
    {
        this.DateOfBirth = customer.DateOfBirth;
        this.Age = customer.Age;
        if (this.DateOfBirth.DayOfYear == DateTime.Today.DayOfYear)
        {
            this.Greeting = "Happy Birthday!!!";
        }
    }
    public int Age { get; set; }

    [Required(ErrorMessage = "Date of birth is required.")]
    [Display(Name = "Data of birth")]
    public DateTime DateOfBirth { get; set; }

    public string Greeting { get; set; }
}

这里有一个问题:您见过多少个带有Display属性的 EF 模型? 我会让您决定 EF 模型是否应该关注它在 UI 中的显示方式。 只是假设我的 EF 模型将显示在 UI 中是错误的。 也许我的班级的唯一消费者是另一个 Web 服务。 我不认为Display应该在 EF 模型中,但有些人可能不同意我的看法; 你打电话。

stackoverflow 上有很多关于人们询问有时需要 PropertyX 而有时不需要的问题,我该怎么做? 好吧,如果您没有在 EF 模型上放置Required属性并在视图中使用您的 EF 模型,那么您就不会遇到此问题。 将有一个视图模型,其中 PropertyX 是必填字段。 该模型将使用Required属性装饰PropertyX,而不需要PropertyX 的视图的另一个模型不会使用Required属性装饰该属性。


视图模型

然后你可能有一个 WPF 应用程序的客户视图模型,你可能有一个前端的 javascript 视图模型(KnockoutJS 视图模型)。


结论和回答你的问题

因此,总而言之,您可以拥有与实体模型不同的域模型。 您的域模型应该不知道数据库。 如果您由于规范化而决定从一个表中删除一列并将其放入自己的表中,那么您的实体模型将受到影响。 您的域模型不应该受到影响。

我在网上读过一些争论,例如“这个设计需要很长时间,我只想快速推出一些东西并把它交给客户并获得报酬”。 好吧,如果您不是在设计需要维护的产品,并且将向其中添加功能,但您只是为您的客户设计一个快速的小站点,那么请不要使用这种方法。 没有一种设计适用于所有情况。 要带走的一点是,您的设计应该在考虑未来的情况下明智地选择。

此外,不需要手动完成从实体模型到域到 MVC 模型的转换。 有一些库可以轻松为您完成此操作,例如AutoMapper

但我不得不承认,网络上有大量示例,并且在许多应用程序中也有使用,其中实体模型在整个应用程序中使用,规则随处可见,并带有大量 if 语句。

这与 DDD 的关系

当我读到你的问题时,我发现了一些引人注目的东西。 就是这个:

我已经阅读了很多关于 DDD 的文章并理解,我应该在基础设施级别使用我的域模型类,因此,我应该使用与实体框架基础设施相同的类并使用它们来生成表(代码优先方法)

老实说,DDD 知识的最佳来源仍然是 Blue Book。 我知道,我知道,它很厚,很难读。 可以看看 DDD Distilled by Vernon。 结论应该是 DDD 并不是真正要处理持久性,而是更深入地了解领域,更好地了解您的领域专家。 当然,它没有说明 ORM。

领域模型持久化

领域模型通常由具有状态和行为的对象(如果我们谈论面向对象模型)组成。 一个模型会有一个或多个实体,并且可能是一些值对象。 在许多情况下,每个有界上下文只有一个实体。 实体被分组在Aggregates 中,它们一起变化,形成事务边界。 这意味着聚合中的每个更改都是一个事务,无论此更改涉及多少个实体。 每个Aggregate都有一个且只有一个实体,即Aggregate Root ,它向其他人公开公共方法以与整个Aggregate一起工作。

所以你的存储库应该照顾:

  • 在一个事务中为新的和更新的对象保留整个聚合(无论有多少实体)
  • 从您的持久性存储中获取整个聚合,通过其身份(聚合根Id属性)

您肯定会需要一些查询,但是只要他们不修改域模型状态,他们就可以查询他们想要的方式。 许多将查询方法添加到存储库,但这取决于您。 我会将它们实现为带有DbContext扩展方法的单独静态类。

型号不匹配

您提到您的持久性模型与域模型不匹配。 这可能是这种情况,尽管在许多情况下并非如此。 有几种方法可以解决这个问题:

  • 将状态与行为分开,并将其作为域对象中的一个属性。 OrderAddLine等等,以及OrderState和所有这些TotalCustomerId和类似的东西。 请记住,这可能不适用于复杂的聚合。
  • 专注于我上面提到的Repository的两个主要方法 - AddGet 每个存储库仅适用于一种类型的聚合,如何在它们之间进行映射取决于您。
  • 结合上面的一点,你可以重新考虑使用ORM并做其他事情。 基本上你可以只使用 ADO.NET,但最简单的是使用某种面向文档的东西,比如 NoSQL,尽管很多人不同意。 另请查看这篇关于 PostgreSQL JSONB 存储作为持久性的文章。

请记住,重点是让Repository为您完成工作,并且可能(可能这永远不会发生,但仍然)使用另一个商店。

您可能还对Vernon 的一篇文章感兴趣,其中他专门讨论了如何使用 EF。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM