Paul Hiles: 3 ways to avoid an anemic domain model in EF Core

1.引言

在使用ORM中(比如Entity Framework)贫血领域模型十分常见 。本篇文章将先探讨贫血模型的问题,再去探究在EF Core中使用Code First时如何使用简单的方法来避免贫血模型。

2.什么是贫血模型

在对领域建模后,输出一系列类中仅包含一些简单属性声明而不包含业务逻辑的模型,就属于贫血模型。当使用Entity Framework时,它们不仅仅是简单的数据持有者而且包含有一堆public getter和public setters:

public class BlogPost
{
    public int Id { get; set; }     [Required]
    [StringLength(250)]
    public string Title { get; set; }     [Required]
    [StringLength(500)]
    public string Summary { get; set; }     [Required]
    public string Body { get; set; }     public DateTime DateAdded { get; set; }     public DateTime? DatePublished { get; set; }     public BlogPostStatus Status { get; set; }
    ...
}

由于其完全缺乏面向对象编程的原则,因此贫血模型通常被描述为反模式。他们需要调用者来完善验证和其他业务逻辑。由于缺乏相应的抽象,就会导致代码重复、较差的数据完整性,以及增加高层模块的复杂性。

贫血模型是十分常见的。从我的经验来看,EF中超过80%的领域模型都是贫血模型。这并不奇怪。几乎所有的文档和其他博客文章都以最简单的方式展示了EF。他们专注于尽可能快地开始工作,而不是主张最佳实践。

3.改造为更丰富的领域模型(充血模型)

下面我们将讨论三种简单的方式去丰富你的贫血模型。这几种方法都非常简单,仅需要最小的改动。

3.1移除无参公共构造函数

除非你指定一个构造函数,否则你的类将有一个默认的无参数构造函数。这意味着你可以用下面的方式实例化你的类:

var blogPost = new BlogPost();

在大多数情况下,这是没有意义的。领域对象通常至少需要一些数据才能使其有效。创建没有任何数据(如标题或URL)的BlogPost实例是没有意义的,因为其仅仅是一个实例化对象,但对象却不包含状态和行为,不满足数据有效性。有些人不同意,但是DDD社区普遍认为确保领域对象始终有效是有意义的。为了解决这个问题,我们可以像处理其他OO类一样对待我们的域类,并引入一个参数化的构造函数:

public BlogPost(string title, string summary, string body)
{
if (string.IsNullOrWhiteSpace(title))
{
throw new ArgumentException("Title is required");
} ... Title = title;
Summary = summary;
Body = body;
DateAdded = DateTime.UtcNow;
}

现在在调用代码必须提供最少的数据来满足约束(构造函数)。这一变化提供了两个积极成果:

  1. 任何新实例化的BlogPost对象现在都保证有效。作用于BlogPost的任何代码都无需检查其有效性。领域对象在实例化时自动校验自身的有效性。
  2. 任何调用代码都知道实例化对象所需的内容。使用无参数的构造函数,很容易构造对象,但却不知道必须要构建的数据才能保证数据有效性。

但不幸的是,在进行此更改后,您将发现在从数据库中检索实体时,您的EF代码不再有效:

InvalidOperationException:在实体类型'BlogPost'上找不到无参数的构造函数。为了创建'BlogPost'的实例,EF需要声明一个无参数的构造函数。

EF需要一个无参数的构造函数来查询该做什么?幸运的是,尽管EF确实需要无参数构造函数,但它并不要求构造函数必须为public,所以我们可以为EF增加一个无参private构造函数,同时强制调用代码使用参数化构造函数。拥有额外的构造函数显然并不理想,但这些妥协通常可以时ORM与OO代码更好地配合。

private BlogPost()
{
// just for EF
} public BlogPost(string title, string summary, string body)
{
...
}

3.2. 删除公共属性中的set方法

上面介绍的参数化构造函数确保在实例化时对象处于有效状态。尽管如此,这并没有阻止您将属性值更改为无效值。要解决这个问题,我们有两个选择:

  1. 将验证逻辑添加到属性设置器
  2. 防止直接修改属性,改为使用与用户操作相对应的方法

向属性设置器添加验证是完全可以接受的,但意味着我们不能再使用自动属性并且必须引入一个后台字段。显然这不是什么大问题:

private string title;

public string Title
{
get { return title; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Title must contain a value");
} title = value;
}
}

第二种方式更受欢迎的主要原因在于它更接近地模拟了现实世界中发生的事情。用户不是孤立地更新单个属性,而是倾向于执行一组已知操作(由UI或API接口确定)。这些操作可能会导致一个或多个属性被更新,但通常情况下更多。业务逻辑依赖于上下文的场景是非常普遍的,这将会导致对属性进行赋值的set中的验证逻辑变得复杂而难以理解。作为基本示例,请考虑以下博客文章发布流程:

public void Publish()
{
if (Status == BlogPostStatus.Draft || Status == BlogPostStatus.Archived)
{
if (Status == BlogPostStatus.Draft)
{
DatePublished = DateTime.UtcNow;
} Status = BlogPostStatus.Published;
}
}

在这个例子中,我们有一个Publish()方法,它有一些简单的逻辑和两个可以更新的属性。我们也可以将其作为一个属性的setter来实现,但它不太清晰,尤其是从另一个类中调用它时:

blogPost.Status = BlogPostStatus.Published;

VS

blogPost.Publish();

第一种方式的副作用是不能清晰的表达业务用例。

当然,你在大多数代码库中看到的是根本不在领域对象中进行验证。相反,这种类型的逻辑可以在下一层找到。这可能导致:

  1. 更长的方法将领域特定的逻辑与编排、持久性和其他关注点混合在一起。
  2. 不同动作之间重复的验证逻辑。
  3. 由于外部依赖性(需要使用Mock)而难以测试纯领域逻辑。

正如我们现在所期望的那样,如果我们从每个属性中彻底移除setter,EF将无法正常运行,但将访问级别更改为private就可以很好地解决问题:

public class BlogPost
{
public int Id { get; private set; }
...
}

这样,所有属性在类之外都是只读的。为了允许更新我们的领域类,我们引入了相应类型动作的方法,如上面所示的Publish方法。

通过删除无参数构造函数和公共属性设置器并添加动作类型的方法,我们现在拥有了始终有效的领域对象,并包含了与所讨论的实体直接相关的所有业务逻辑,这是一个很大的改进。我们已经使我们的代码同时更加健壮和简单。

虽然我们可以讨论其他DDD概念,例如领域事件以及通过双派遣模式(double-dispatch pattern)使用领域服务,但它们的优势,特别是简单性方面的优势远不是那么明显。

通常DDD概念中可以简化代码的是我们将在下面讨论的值对象的使用。

3.3.引入值对象

值对象是不可变的(实例化后不允许更改)没有身份标识的对象。值对象通常可以用来代替领域对象中的一个或多个属性。

值对象的经典示例包括货​​币,地址和坐标,但也可以使用值类型替换单个属性,而不是使用字符串或整型。例如,不是将电话号码存储为字符串,而是可以创建一个带有内置验证的PhoneNumber值类型以及提取拨号代码的方法等。

下面的代码显示了一个实现为EF类使用的货币值对象:

public class Money
{
[StringLength(3)]
public string Currency { get; private set; } public int Amount { get; private set; } private Money()
{
// just for EF
} public Money(string currency, int amount)
{
// todo validation
Currency = currency;
Amount = amount;
}
}

货币和金额是内在联系的。为了使数据有效,这两条信息都是必需的。因此,对它们进行建模是有道理的。请注意,参数化的构造函数和私有属性设置器的使用方式与我们在建模领域对象时所使用的完全相同。实体框架也需要一个私有无参数构造函数。

在(RDBMS)数据持久性的上下文中,值类型不存在于单独的数据库表中。为了让我们在实体框架中使用值对象,需要一个小的改动。这取决于您使用的EF版本。

在EF6中,我们只需用[ComplexType]属性修饰值对象:

[ComplexType]
public class Money
{
...
}

在EF Core中,从版本2开始,我们可以使用Fluent API中不常用的OwnsOne方法:

public class BlogContext : DbContext
{
...
public DbSet<BlogPost> BlogPosts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BlogPost>().OwnsOne(x => x.AdvertisingFee);
}
}

这里假定在我们的BlogPost实体上使用Money值对象,如下所示:

public class BlogPost
{
...
public Money AdvertisingFee { get; private set; }
...
}

创建并运行迁移后,我们会发现我们的数据库表现在包含两个额外的列:

AdvertisingFee_Currency
AdvertisingFee_Amount

使用值对象的好处与向富领域模型的转变非常相似。丰富的领域模型不需要调用代码来验证领域模型,并提供了一个定义良好的抽象来进行编程。一个值对象进行自我验证,因此包含值对象属性的领域模型本身不需要知道如何验证值类型。所有非常清晰和简单。

4. 温馨提示

当您打算从贫血域模型转移到更丰富的领域模型时,您将立即体会到将领域级的业务逻辑封装在领域对象中的好处。请注意,尽管如此,尝试并不是件容易的事。在您的领域对象上创建一个方法来执行验证,然后更新多个属性无疑是件好事。但从领域对象发送电子邮件或保存到数据库并不是您可能想要做的事情。重要的是要意识到,拥有丰富的领域模型并不否定另一层的需求来安排这些更高层次的关注。这是应用服务或命令处理程序的工作,具体取决于您的体系结构。

5.关于单元测试的说明

一个丰富的、自我验证的领域模型的一个负面影响是它可以使测试变得更加困难。通过public setter,您可以简单地将各个值分配给任何领域对象的属性。这使您可以直接指定您需要的确切值,以便将对象置于特定状态以进行测试。如果你锁定你的属性和构造函数,那么这种方法是不可能的。但这也不是一件坏事,它使单元测试变得稍微困难​​一点,但你所做的是确保你的测试是有效的。

另一方面,它也使得测试领域对象本身的逻辑非常简单。尽管你的应用服务/命令处理程序的单元测试几乎肯定会需要一定程度的模拟,但你应该发现大部分领域对象测试的构建要简单得多,并且通常不需要依赖模拟。

6. 总结

本文介绍了三种非常简单的技术,您可以使用Entity Framework和EF Core从贫血域模型转换为更为丰富的领域模型。使用参数化的构造函数可以确保我们的领域模型在实例化时有效。清除公共属性setter确保我们的模型在其整个生命周期内保持有效状态。在领域模型上内部执行验证和引入更改状态的方法使我们能够集中业务逻辑并简化调用代码。最后,我们考察了值对象的使用,并解释了他们如何进一步推进了这种简化和逻辑封装。

EF Core中避免贫血模型的三种行之有效的方法(翻译)的更多相关文章

  1. EF Core 中DbContext不会跟踪聚合方法和Join方法返回的结果,及FromSql方法使用讲解

    EF Core中: 如果调用Queryable.Count等聚合方法,不会导致DbContext跟踪(track)任何实体. 此外调用Queryable.Join方法返回的匿名类型也不会被DbCont ...

  2. 【css笔记】css中的盒模型和三种定位机制(固定定位,绝对定位,浮动)

    html页面上的元素都可以看成是框组成的,框通过三种定位机制排列在一起就过程了我们看到的页面.而框就是盒模型. 盒模型 1.页面上的每个元素可以看成一个矩形框,每个框由元素的内容,内边距,边框和外边距 ...

  3. EF Core中DbContext可以被Dispose多次

    我们知道,在EF Core中DbContext用完后要记得调用Dispose方法释放资源.但是其实DbContext可以多次调用Dispose方法,虽然只有第一次Dispose会起作用,但是DbCon ...

  4. EF Core中执行Sql语句查询操作之FromSql,ExecuteSqlCommand,SqlQuery

    一.目前EF Core的版本为V2.1 相比较EF Core v1.0 目前已经增加了不少功能. EF Core除了常用的增删改模型操作,Sql语句在不少项目中是不能避免的. 在EF Core中上下文 ...

  5. EF Core中通过Fluent API完成对表的配置

    EF Core中通过Fluent API完成对表的配置 设置实体在数据库中的表名 通过ToTable可以为数据模型在数据库中自定义表名,如果不配置,则表名为模型名的复数形式 public class ...

  6. 9.翻译系列:EF 6以及EF Core中的数据注解特性(EF 6 Code-First系列)

    原文地址:http://www.entityframeworktutorial.net/code-first/dataannotation-in-code-first.aspx EF 6 Code-F ...

  7. EF Core 中多次从数据库查询实体数据,DbContext跟踪实体的情况

    使用EF Core时,如果多次从数据库中查询一个表的同一行数据,DbContext中跟踪(track)的实体到底有几个呢?我们下面就分情况讨论下. 数据库 首先我们的数据库中有一个Person表,其建 ...

  8. EF Core中如何正确地设置两张表之间的关联关系

    数据库 假设现在我们在SQL Server数据库中有下面两张表: Person表,代表的是一个人: CREATE TABLE [dbo].[Person]( ,) NOT NULL, ) NULL, ...

  9. 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获

    项目开发中的一些注意事项以及技巧总结   1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...

随机推荐

  1. 搭建Linux运行环境-虚拟机

    1.虚拟机软件介绍 虚拟机(Virtual Machina)软件就是一套特殊的软件,它可以作为系统独立运行,也可以运行与系统之上. 若是运行与系统之上的虚拟机软件,在一台电脑(PC或笔记本等)上安装虚 ...

  2. golang自动构建脚本

    #!/bin/sh #代码分支 branch_c=$ branch_p=$ #服务器 server=$ #构建版本 version=$ case $server in test1) echo &quo ...

  3. 下拉框、下拉控件之Select2。自动补全的使用

    参考链接: 参考一:https://blog.csdn.net/weixin_36146275/article/details/79336158 参考二:https://www.cnblogs.com ...

  4. Mybatis 源码学习系列

    前言 很久以前,我们学习了Java,从一个控制台的 Hello world .开始,我们进入了面向对象的世界. 然后由学习了SQL语言,可以写出SQL语句来将尘封在硬盘之下的数据库数据,展现出来. 后 ...

  5. thinkPHP5扩展workerman

    -安装workerman 首先通过 composer 安装 composer require topthink/think-worker -vvv 如果报错: Installation failed, ...

  6. 《ServerSuperIO Designer IDE使用教程》-3.Modbus协议,读取多个寄存器,实现多种数据类型解析。发布:v4.2.2版本

    更新内容,v4.2.2版本:1.增加Modbus协议读取多个寄存器,并且按多种数据类型解析数据.2.Modbus Serial和Modbus TCP两个驱动合并成一个驱动.3.修改数据库结构,保存配置 ...

  7. linux上部署JMeter

    export JAVA_HOME=/opt/jdk1.8.0_171 export PATH=$PATH:$JAVA_HOME/bin 让环境变量生效 vi /etc/profile 添加下述两行: ...

  8. java笔记(Idea,Maven):误删maven项目的target的class,怎么再生成target

    右边侧边栏clean一下,target目录删掉了.或是手动删掉了.再建. 跑一下 Tomcat.   target自动生成. 就这样.:)

  9. 三、自动化测试平台搭建-django-如何用mysql数据库做web项目

    从这节开始到后面说的大概内容如下: 这里说的是Django做一个web项目的大概框架,从下篇具体说Django中的模型(查询..),视图(请求,响应,cookie,session..),模板(验证码, ...

  10. Python ftplib模块

    Python ftplib模块 官方文档:https://docs.python.org/3/library/ftplib.html?highlight=ftplib#module-ftplib 实例 ...