EF Core中避免贫血模型的三种行之有效的方法(翻译)

640?wx_fmt=png

[Paul Hiles: 3 ways to avoid an anemic domain model in EF Core :https://www.devtrends.co.uk/blog/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:

  1. public class BlogPost

  2. {

  3.     public int Id { get; set; }

  4.     [Required]

  5.     [StringLength(250)]

  6.     public string Title { get; set; }

  7.     [Required]

  8.     [StringLength(500)]

  9.     public string Summary { get; set; }

  10.     [Required]

  11.     public string Body { get; set; }

  12.     public DateTime DateAdded { get; set; }

  13.     public DateTime? DatePublished { get; set; }

  14.     public BlogPostStatus Status { get; set; }

  15.     ...

  16. }

由于其完全缺乏面向对象编程的原则,因此贫血模型通常被描述为反模式。他们需要调用者来完善验证和其他业务逻辑。由于缺乏相应的抽象,就会导致代码重复、较差的数据完整性,以及增加高层模块的复杂性。 贫血模型是十分常见的。从我的经验来看,EF中超过80%的领域模型都是贫血模型。这并不奇怪。几乎所有的文档和其他博客文章都以最简单的方式展示了EF。他们专注于尽可能快地开始工作,而不是主张最佳实践。

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

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

3.1.移除无参公共构造函数

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

  1. var blogPost = new BlogPost();

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

  1. public BlogPost(string title, string summary, string body)

  2. {

  3.    if (string.IsNullOrWhiteSpace(title))

  4.    {

  5.        throw new ArgumentException("Title is required");

  6.    }

  7.    ...

  8.    Title = title;

  9.    Summary = summary;

  10.    Body = body;

  11.    DateAdded = DateTime.UtcNow;

  12. }

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

  1. 任何新实例化的BlogPost对象现在都保证有效。作用于BlogPost的任何代码都无需检查其有效性。领域对象在实例化时自动校验自身的有效性。

  2. 任何调用代码都知道实例化对象所需的内容。使用无参数的构造函数,很容易构造对象,但却不知道必须要构建的数据才能保证数据有效性。

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

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

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

  1. private BlogPost()

  2. {

  3.    // just for EF

  4. }

  5. public BlogPost(string title, string summary, string body)

  6. {

  7.    ...

  8. }

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

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

  1. 将验证逻辑添加到属性设置器

  2. 防止直接修改属性,改为使用与用户操作相对应的方法

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

  1. private string title;

  2. public string Title

  3. {

  4.    get { return title; }

  5.    set

  6.    {

  7.        if (string.IsNullOrWhiteSpace(value))

  8.        {

  9.            throw new ArgumentException("Title must contain a value");

  10.        }

  11.        title = value;

  12.    }

  13. }

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

  1. public void Publish()

  2. {

  3.    if (Status == BlogPostStatus.Draft || Status == BlogPostStatus.Archived)

  4.    {

  5.        if (Status == BlogPostStatus.Draft)

  6.        {

  7.            DatePublished = DateTime.UtcNow;

  8.        }

  9.        Status = BlogPostStatus.Published;

  10.    }

  11. }

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

  1. blogPost.Status = BlogPostStatus.Published;

VS

  1. blogPost.Publish();

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

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

  1. 更长的方法将领域特定的逻辑与编排、持久性和其他关注点混合在一起。

  2. 不同动作之间重复的验证逻辑。

  3. 由于外部依赖性(需要使用Mock)而难以测试纯领域逻辑。

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

  1. public class BlogPost

  2. {

  3.    public int Id { get; private set; }

  4.    ...

  5. }

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

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

虽然我们可以讨论其他DDD概念,例如领域事件以及通过双派遣模式([double-dispatch pattern:http://idior.cnblogs.com/articles/325036.html])使用领域服务,但它们的优势,特别是简单性方面的优势远不是那么明显。 通常DDD概念中可以简化代码的是我们将在下面讨论的值对象的使用。

3.3.引入值对象

[值对象:https://martinfowler.com/bliki/ValueObject.html]是不可变的(实例化后不允许更改)没有身份标识的对象。值对象通常可以用来代替领域对象中的一个或多个属性。

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

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

  1. public class Money

  2. {

  3.    [StringLength(3)]

  4.    public string Currency { get; private set; }

  5.    public int Amount { get; private set; }

  6.    private Money()

  7.    {

  8.        // just for EF

  9.    }

  10.    public Money(string currency, int amount)

  11.    {

  12.        // todo validation

  13.        Currency = currency;

  14.        Amount = amount;

  15.    }

  16. }

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

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

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

  1. [ComplexType]

  2. public class Money

  3. {

  4.    ...

  5. }

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

  1. public class BlogContext : DbContext

  2. {

  3.    ...

  4.    public DbSet<BlogPost> BlogPosts { get; set; }

  5.    protected override void OnModelCreating(ModelBuilder modelBuilder)

  6.    {

  7.        modelBuilder.Entity<BlogPost>().OwnsOne(x => x.AdvertisingFee);

  8.    }

  9. }

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

  1. public class BlogPost

  2. {

  3.    ...

  4.    public Money AdvertisingFee { get; private set; }

  5.    ...

  6. }

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

  1. AdvertisingFee_Currency

  2. AdvertisingFee_Amount

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

4. 温馨提示

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

5.关于单元测试的说明

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

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

6. 总结

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/317079.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[ONTAK2010] Peaks加强版 (kruskal重构树+主席树+倍增)

Peaksdescriptionsolutioncodedescription 在Bytemountains有N座山峰&#xff0c;每座山峰有他的高度h_i 有些山峰之间有双向道路相连&#xff0c;共M条路径&#xff0c;每条路径有一个困难值&#xff0c;这个值越大表示越难走 现在有Q组询问&#xff0c;每组询问询问从点v开…

洛谷P2754:[CTSC1999]家园 / 星际转移问题(网络流)

解析 容易想到对每个时间的空间站都建一个点。 然后发现循环问题很难搞。 然后我就一直想从 lcm 下文章&#xff0c;结果 lcm 可以到3e5&#xff0c;于是就寄了… qwq 注意到本题的数据范围极小&#xff01; 那个3e5云云是不可能跑出来的&#xff0c;事实上&#xff0c;答案不…

Saving Beans HDU - 3037(卢卡斯定理)

Saving Beans HDU - 3037&#xff08;卢卡斯定理&#xff09; 题意&#xff1a; 他们想知道有多少种方法可以在n树中保存不超过m个bean&#xff08;它们是相同的&#xff09;。 现在他们求助于你&#xff0c;你应该给他们答案。 结果可能非常巨大; 你应该输出模p的结果&…

我们为什么要搞长沙.NET技术社区(三)

我们为什么要搞长沙.NET技术社区&#xff08;三&#xff09; 小饭局搞事情先从饭局开始是中华民族的优良传统。昨天晚餐时间&#xff0c;长沙 .net 技术社区的主要发起人员进行了一番小聚&#xff0c;同时也作为一个非正式会议&#xff0c;对社区发展进行了探讨。从介绍自己对于…

BZOJ4504. K个串(主席树+优先队列)

4504. K个串descriptionsolutioncodedescription 兔子们在玩k个串的游戏。首先&#xff0c;它们拿出了一个长度为n的数字序列&#xff0c;选出其中的一 个连续子串&#xff0c;然后统计其子串中所有数字之和&#xff08;注意这里重复出现的数字只被统计一次&#xff09;。 兔…

【招聘(北京)】北森测评招聘 .NET 架构师、高级工程师

工作职责公司核心产品的迭代需求分析设计开发。公司核心产品的线上维护和性能调优。对初中级技术人员培养和质量把关。编写软件设计和技术文档。任职资格为人正直、诚信、责任心强&#xff0c;能承受较大工作压力。强烈的目标导向意识&#xff0c;逻辑思维清晰&#xff0c;执行…

网络流模型与技巧总结

文章目录前言常见基本模型最大匹配、最小点覆盖和最大独立集构造最小点覆盖最大点权匹配最小路径覆盖不可重覆盖可重覆盖最大权闭合子图建图技巧利用拆点进行限流利用断边表示决策利用虚点表示组合关系链状模型用链表示时间轴用链表示偏序关系形式的选取限制通过拆点描述先后顺…

卢卡斯定理 Lucas

参考文章 详细定义内容看这个参考文章 结论&#xff1a; 模板&#xff1a; Lucas函数&#xff1a; long long Lucas(long long n,long long m){if(m0) return 1;return Lucas(n/p,m/p)*C(n%p,m%p)%p; }组合数函数&#xff1a; 此处求逆元的用的bp-2 long long C(long long…

BZOJ #2874. 训练士兵(差分+离散化+主席树)

BZOJ #2874. 训练士兵descriptionsolutioncodedescription Ryz正在着手于训练一批精锐士兵 Ryz手下有n*m个士兵&#xff0c;排成一个n行m列的方阵。在一天中&#xff0c;ryz会对士兵下达一些命令&#xff0c;每个命令作用于一个小方阵的所有士兵&#xff0c;并且会增加他们的…

VS 2019 要来了,是时候了解一下 C# 8.0 新功能

近日&#xff0c;微软发布了 Visual Studio 2019 的发布日期&#xff0c;2019 年 4 月 2 日 Visual Studio 2019 将正式和大家见面&#xff0c;同时微软还将提供发布现场实时直播。除了 Visual Studio 2019 自身之外&#xff0c;VS 2019 的发布还牵动着很多 C# 开发者的心。虽然…

[蓝桥杯2020国赛]游园安排

题目&#xff1a; 题解&#xff1a; 本质就是求最长上升子序列&#xff0c;只不过这里是字符串版本的&#xff0c;我们都知道有n^2的LIS&#xff0c;但其实还有O(nlogn)版本的&#xff0c;详细看这里&#xff0c;套上就行 另外我发现这里竟然有蓝桥杯全套的编程题离谱&#xf…

洛谷P2761:软件补丁问题(状压、分层图最短路)

当遇到瓶颈时&#xff0c;想想自己是否做了可以优化的无用功。 解析 不难想到状压最短路的做法。 但是直接加边的话边数会是 O(m2n)O(m2^n)O(m2n) 级别&#xff0c;只有 909090 分&#xff0c;难以通过。 注意到&#xff0c;由于补丁很少&#xff0c;真正能达到的状态是很有限…

[2020-09-11 CQBZ/HSZX多校联测 T3] 万猪拱塔(线段树+巧妙转化)

万猪拱塔descriptionsolutioncodedescription 题目描述 小七养了很多头猪&#xff0c;它们分布在 n 行 m 列中&#xff0c;其中第 i 行第 j 列的猪圈养的是第 wi,jw_{i,j}wi,j​ 种猪。 小七有时会选择一个子矩形范围内的猪圈进行巡视&#xff0c;如果该子矩形包含 i 行 j 列 …

重新解读DDD领域驱动设计(一)

回顾十年前&#xff0c;还未踏入某校时&#xff0c;便听闻某学长一毕业就入职北京某公司&#xff0c;月薪过万。对于一个名不见经传的小学院&#xff0c;一毕业能拿到这个薪水还是非常厉害的。听闻他学生期间参与开发了一款股票软件&#xff0c;股票那时正迎来一波疯涨。时也运…

Tickets HDU - 1260

Tickets HDU - 1260 题意&#xff1a; 著名的宫崎骏动画片《千与千寻》在中传1500报重映&#xff0c;中传学子有幸成为全国第一批观众。动画学院学生会负责给大家发票&#xff0c;所有的中传同学只要把中传首映《千与千寻》的推送信息发到朋友圈并集够10个赞&#xff0c;就可…

洛谷P3357:最长k可重线段集问题(网络流)

解析 本题的建模方法有很多&#xff0c;我的做法是补集思想转化成志愿者招募然后按照那道题的做法直接做&#xff0c;看题解更多是采用的对于不冲突的线段首尾加边的做法。 在前一道最长k可重区间问题中这两种做法谈不上孰优孰劣&#xff0c;但本题中题解的做法在处理垂直线段…

程序员修神之路--高并发优雅的做限流(有福利)

点击上方蓝色字体&#xff0c;关注我们菜菜哥&#xff0c;有时间吗&#xff1f;YY妹&#xff0c;什么事&#xff1f;我最近的任务是做个小的秒杀活动&#xff0c;我怕把后端接口压垮&#xff0c;X总说这可关系到公司的存亡简单呀&#xff0c;你就做个限流呗这个没做过呀&#x…

BZOj #4771. 七彩树(主席树+dfn序+lca)

BZOj #4771. 七彩树descriptionsolutioncodedescription 给定一棵n个点的有根树&#xff0c;编号依次为1到n&#xff0c;其中1号点是根节点。每个节点都被染上了某一种颜色&#xff0c;其中第i个节点的颜色为c[i]。如果c[i]c[j]&#xff0c;那么我们认为点i和点j拥有相同的颜色…

免费馅饼 HDU - 1176

免费馅饼 HDU - 1176 题意&#xff1a; 都说天上不会掉馅饼&#xff0c;但有一天gameboy正走在回家的小径上&#xff0c;忽然天上掉下大把大把的馅饼。说来gameboy的人品实在是太好了&#xff0c;这馅饼别处都不掉&#xff0c;就掉落在他身旁的10米范围内。馅饼如果掉在了地上…

CF1631F:Flipping Range(dp)

解析 设 x,y∈B,x<yx,y\in B,x<yx,y∈B,x<y&#xff0c;那么也有 x−y∈Bx-y\in Bx−y∈B。 递归下去&#xff0c;根据辗转相减求 gcd⁡\gcdgcd 的方法可知&#xff0c;最终会得到 gcd⁡(x,y)\gcd(x,y)gcd(x,y)。 那么对于整个集合 BBB &#xff0c;它也就等价于所有…