如何运用DDD - 实体

概述

本文将介绍领域驱动设计(DDD)战术模式中另一个常见且非常重要的概念 - 实体。相对战术模式中其他的一些概念(例如 值对象、领域服务等)来说,实体应该比较容易让人理解和运用。但是我们如何去发现所在领域中的实体呢?如何保证建立的实体是富含行为的?实体运用时又有那些注意的细节呢?本文将从不同的角度来带大家重新认识一下“实体”这个概念,并且给出相应的代码片段(本教程的代码片段都使用的是C#,后期的实战项目也是基于 DotNet Core 平台)。

何为实体

按照国际惯例呢,我们先吹牛。直接来看看原著《领域驱动设计:软件核心复杂性应对之道》 中对实体的解释:

  • 实体(Entity,又称为Reference Object) 很多对象不是通过他们的属性定义的,而是通过一连串的连续事件和标识定义的。

  • 主要由标识定义的对象被称为ENTITY。

上面的两句话多读了几遍,好像这个定义还是能够理解嘛。不像上一篇文章 如何运用DDD - 值对象 中的概念那么深奥。说白了,上面就是说明了一个问题,只要你所发现的事物/对象有一个唯一的标识,那么它可能就是实体了。而唯一的标识就是我们代码中快写烂了的那个ID。

似曾相识

来想一下,我们在以传统的设计思路和开发过程中,我们会在什么情况下为一个对象赋予一个ID呢?给它赋予这个ID的作用呢?一般来说我们的目的无非就是 1、为了区分本对象,如果是在数据库中,那就是为了区分本条数据和另外一条数据,而这个ID也往往作为主键而存在 2、加个索引吧,来提升关联查找速度。所以我们如果将数据库中的表映射到我们的代码中以类的形式呈现的时候,它可能就是这个样子:

//旅行的行程
public class Itinerary
{
public int ID { get; set; }

//参加本次旅行的人员
public List<Person> Participants { get; set; }

//旅行的地点
public List<string> Places { get; set; }

//关于该行程的备注笔记信息
public string Note { get; set; }

//旅行开始时间
public DateTime StartTime { get; set; }

//旅行开始时间
public DateTime? EndTime { get; set; }

//旅行的状态(进行中 or 已完成)
public int Status { get; set; }
}

上面的代码对我们来说应该丝毫都不陌生,我们建立了一个旅行行程的类,至于为什么我们会选取旅行行程,而不是各个博客都出现的以订单啊电商平台作为案例。那是因为在后期我们会一起动手来实现一个旅行记账的微信小程序,并且借助于我们慢慢所学习到的DDD理论作为基础,开发属于我们自己的领域驱动框架,当然项目也是基于 DotNet Core(版本应该是3.x)。

好了,还是回到我们这个例子,来思考一下ID出现的目的。你可能会说:“这还不简单吗?老夫纵横代码界多年,你现在还来问我这个问题!ID肯定是用来区分的呀,行程千千万万,我要找出这一条行程肯定需要这个ID了呀。” 是的,这是一个毫无争议的问题。我们需要一个唯一的身份标识来区别对象之间的差异。DDD中实体的这一点与我们平时所接触的类的ID有异曲同工之妙,所以本文开头也说了实体可能是相对其他战术概念最为让人理解的。

你确定它真的需要ID吗

还记得我们在上一篇文章 如何运用DDD - 值对象 中所提到过的一个问题吗?“当前上下文的值对象可能是另一个上下文的实体”。所以说,当前你所判定的实体一定是基于领域当前环境(上下文)的。脱离了该环境之后,一切都将存在变数。同样的事物(对象),在当前环境需要一个唯一标识来识别它,而在另一个环境中可能这个唯一标识对它来说是没有意义的,则实体就有可能成为了值对象。请考虑下面的这个例子:

在一个银行业应用程序中,一位顾客可能会在她的银行账户中放入100美元。当她未来某一天提取她这100美元时,相较于她存进银行的钱,她可能会收到不同的钞票或硬币。不过,这一差异是无关紧要的,因为资金的身份不重要;顾客只关心资金的价值。所以在这个领域中,资金无疑是一个值对象。但在另一个领域中,比如涉及钞票印刷制作或钞票可追溯性的行业,个体钞票或硬币的身份实际上可能就是一个重要的领域概念了。所以每一张钞票都会是一个具有唯一标识符的实体

运用实体

结合值对象

千万不要忘记了我们上一章所学习到了的值对象:在实体的内部,除了它自己的唯一标识ID之外,也许还有许许多多表明它属性的东西,而这些东西往往可以通过使用值对象来标识。接下来让我们来改写一下上面的Itinerary类:

public class Itinerary
{
public int ID { get; set; }

public List<Person> Participants { get; set; }

public List<Address> Places { get; set; }

public ItineraryNote Note { get; set; }

public ItineraryTime TripTime { get; set; }

public ItineraryStatus Status { get; set; }
}

public class ItineraryNote
{
public string Content { get; set; }
public DateTime NoteTime { get; set; }

public ItineraryNote(string content)
{
Content = content;
NoteTime = DateTime.Now;
}
}

为实体赋予它的行为

当对象建立好了之后,为了实现我们的业务逻辑处理,我们需要对实例化的对象进行操作。现在我们为该系统提出第一个需求:用户可以修改行程中的备注信息。回到我们的第一版代码中,如果我们需要处理这个操作,我们会怎么做呢?

itineraryInstance.Note = "this is my new note info";

是不是会像上面这样,将需要添加的值赋予实例化的对象呢。这种操作,对我们现在正在进行的编程习惯来说,是再正常不过了。

那么我们来思考,如果我们的项目有多处需要对“备注信息”处理呢。则对该属性的变更将被散落在代码各处。而当我们对该需求进行了一个增强验证时,比如此时我们需要增加:用户修改行程中的备注信息时,只允许用户录入200个字以内的文本。OMG,此时我们需要去查找所有散落的片段,并且为他加上验证。

从另外个角度来看,第一个版本我们所建立的类,我们无法通过仅仅查看它本身就能读懂有关旅行行程有关的业务,我们仅仅知道它具有起始时间,备注信息等,而对他们应该如何相互作用无从所知。所以这种仅仅具有类的属性,或者说以POCO呈现的类型,我们称之为**“贫血模型”**。

接下来,我们回到第二版代码中,我们为它赋予属于它的行为。从需求中我们得知了,行程的备注信息是可以修改的,而备注信息是属于行程的,因此修改备注信息改行为理应属于行程本身。我们稍微改动代码:

public class Itinerary
{
public int ID { get; set; }

public List<Person> Participants { get; set; }

public List<Address> Places { get; set; }

public ItineraryNote Note { get; set; }

public ItineraryTime TripTime { get; set; }

public ItineraryStatus Status { get; set; }

//ctor

public void ChangeNote(string content)
{
if(content.Length > 200 )
throw new NoteIsOverlengthException();
Note = new ItineraryNote(content);
}
}

此时我们为Itinerary赋予了一个ChangeNote的行为,当外界需要更改备注时,则只需通过调用改方法既可以实现,而且当展开其他开发人员阅读此类时,也会清楚的明白,业务上允许用户更改200字以内的备注。

但是,我们依然有一个地方美中不足,我想你可能也发现了:属性还是对外暴露的!对,也就是说,我们除了通过类公开的行为修改类自身的属性外,我们还可以在外界随意更改。这显然不符合我们设计的初衷。因此我们可以将所有属性的set私有化。所以,一定要注意,我们在考虑实体的时候,一定要知道“实体是高度内聚和自治的”(敲重点!!!!!)。

当然,有的开发者还会尝试另外的写法,让实体完全自治,将上面的代码中的属性,全部转变为私有的字段,外界只能通过公开的行为来对实体进行处理。

public class Itinerary
{
public int ID { get; set; }

private List<Person> participants;

private List<Address> places;

private ItineraryNote note;

private ItineraryTime tripTime;

private ItineraryStatus status;

//ctor

public void ChangeNote(string content)
{
if(content.Length > 200 )
throw new NoteIsOverlengthException();
note = new ItineraryNote(content);
}
}

但是当外界需要获取该实体的值,或者需要ORM映射的时候可能就不是很友好了,不过你可以使用类似于像 备忘录模式 的快照方法来处理。后期我们也会采用这种模式来实现部分案例。

通过将实体赋予它应用的行为所建立出来的实体我们称为“充血模型”。那么贫血模型好还是充血模型好呢?很多同学肯定会说,这还用问吗,肯定是充血模型啦。其实这个答案并没有一个真正的答案,实体自身的行为是通过我们对领域的慢慢分析(可能是通过与领域专家沟通)得来的,如果因为为了使用充血模型而盲目的将一些不属于实体的行为赋予给它,只会让实体变的更加混乱,从而得不偿失。所以,此时的贫血模型并不意味着一直是贫血模型,后期随着领域的深入它可能会不断丰富属于自身的行为。

尝试转移一部分行为给值对象

保持实体专注于身份这一职责很重要,因为这样会避免它们变得臃肿————这是它们将许多相关行为拉到一起时容易掉入的陷阱。实现这一专注需要将相关行为委托给值对象和领域服务(领域服务也将在后期的文章中进行介绍)。来考虑一下最近一版的代码,我们已经将行为划分给了Itinerary了,但是仔细看一看,我们在后期增加需求时增加了一条验证的规则,那么这个规则我们可以转移给值对象吗?答案是,可以的。而且转移是有必要的,因为对备注的效验这一行为往往应该属于它自身。就好比机器启动时的自我效验,这一行为是属于操作者还是机器自己呢?所以我们来将部分行为转移给值对象,优化后的代码可能是这样的:

public class Itinerary
{
public int ID { get; set; }

public List<Person> Participants { get; set; }

public List<Address> Places { get; set; }

public ItineraryNote Note { get; set; }

public ItineraryTime TripTime { get; set; }

public ItineraryStatus Status { get; set; }

//ctor

public void ChangeNote(string content)
{
Note = new ItineraryNote(content);
}
}

public class ItineraryNote
{
public string Content { get; set; }
public DateTime NoteTime { get; set; }

public ItineraryNote(string content)
{
if(content.Length > 200 )
throw new NoteIsOverlengthException();
Content = content;
NoteTime = DateTime.Now;
}
}

愿景是美好的 现实是残酷的

到这里,我们仿佛真的一帆风顺:建立了属于自己的实体,并且融合了该有的值对象,实体的行为也被高度内聚在了其中。那是不是我们直接就可以将DDD落地了呢?不好意思,就如同这个小标题一样,现实真的是非常残酷的。如果单单从代码阅读和业务处理上来说,我们可能确实已经成功了,但是!!!我们需要保存我们的数据,也就是持久化。因为实体中包含了大量的值对象,所有值对象持久化所面临的问题,它都会遇到,甚至是让难度翻倍!有关值对象持久化的难点可以参考上一篇文章 如何运用DDD - 值对象 。

回看我们最后一版代码,我们有两个集合的属性(Participants、Places)。单一的值对象的持久化已经让我们头痛了,现在我们不得不面对持久化值对象集合的问题。假如你通过使用EF Core这类的ORM框架来进行持久化操作,你会发现我们不得不为List中的值对象加上一个ID,此时拥有了唯一标示的值对象显然已经成为了实体,这是非常可怕的一件事。我们辛辛苦苦建立的领域模型在最后一步落地时居然成为改变了,这往往也是DDD落地困难的一个重要原因,被ORM框架或者关系型数据库所限制,导致领域模型不断被打乱,重构领域模型变得越来越四不像,最终又写回了传统的三层架构或者面向数据库建模。

但是至少在现在,请相信自己的所见,认真考虑和发现你项目领域所拥有的值对象和实体,不要因为知道持久化的问题而放弃和妥协,这也是我们开发者应有的勇气。在后面的文章中,我们会关于值对象和实体的一些问题提出解决办法,当然包括持久化的问题。

总结

本文我们介绍了实体的概念以及怎么去运用实体到实际代码中,请牢记前人为我们提供的有关实体的经验:比如**“实体一定是基于领域当前环境(上下文)的”“实体是高度内聚和自治的”“应该专注于实体的行为而非数据”**等等。后面的文章会为大家带来实体和值对象的一些注意事项以及领域服务的内容。

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

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

相关文章

数据结构 最大堆和最小堆

前言&#xff1a; 关于最大堆和最小堆的题目&#xff0c;我做过了很多次&#xff0c;但是好像没有一个是完全独立AC的。2021春季PAT考试的前一天&#xff0c;天梯赛校内赛还专门考了最小堆&#xff0c;我复习了一下&#xff0c;结果第二天PAT考了最大堆&#xff0c;又是没做出来…

Natasha v2.5.4 版与运行时实战

文章转载授权级别&#xff1a;BNatasha 是一个十分便捷的动态构建库&#xff0c;支持.NET Standard2.0 / Core3.0 ; 比起繁杂的 IL 指令和 Expression 的众多 API , Natasha 的构建方式更加友好简洁&#xff0c; 基于 Natasha 可以让动态工作变得傻瓜&#xff0c;简单&#xf…

数据结构 堆中的路径(最小堆)

题目&#xff1a; 将一系列给定数字插入一个初始为空的小顶堆H[ ]。随后对任意给定的下标i&#xff0c;打印从H[i]到根结点的路径。 输入格式: 每组测试第1行包含2个正整数N和M(≤1000)&#xff0c;分别是插入元素的个数、以及需要打印的路径条数。下一行给出区间[-10000, 100…

.Net Core3.1下使用Swagger搭建web api项目

前言&#xff1a;微软于前天发布.net core 3.1正式版,并将长期支持3.1。所以我听到这个消息后就急忙下载.net core 3.1的SDK和Runtime,应该是公司最先用3.1的攻城狮了????。OK&#xff01;废话少说&#xff0c;今天的目的是基于.net core 3.1建一个web api的项目先下载.net…

一个值得学习的WPF开源项目

项目介绍此项目应用了Prism MVVM框架&#xff0c;项目展示数据来源于其他服务程序&#xff0c;使用的WebAPI通信&#xff0c;如果要正常运行此程序&#xff0c;需要您自己做一个WebAPI程序&#xff0c;由API接口提供数据驱动&#xff0c;其实直接查看代码最直接&#xff0c;有需…

程序员修神之路--打通Docker镜像发布容器运行流程

菜菜哥&#xff0c;我看了一下docker相关的内容&#xff0c;但是还是有点迷糊还有哪不明白呢&#xff1f;如果我想用docker实现所谓的云原生&#xff0c;我的项目该怎么发布呢&#xff1f;这还是要详细介绍一下docker了Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言…

PTA 三足鼎立 (lower_bound()+upper_bound())

当三个国家中的任何两国实力之和都大于第三国的时候&#xff0c;这三个国家互相结盟就呈“三足鼎立”之势&#xff0c;这种状态是最稳定的。 现已知本国的实力值&#xff0c;又给出 n 个其他国家的实力值。我们需要从这 n 个国家中找 2 个结盟&#xff0c;以成三足鼎立。有多少…

用.NET解索尼相机ARW格式照片

用.NET解索尼相机ARW格式照片目前常用的照片格式是 .jpg&#xff0c;它只能提供 8bit的色彩深度&#xff0c;而目前主流的相机都能提供高达 12bit- 14bit的色彩深度&#xff0c;动态范围和后期处理能力也大大增加&#xff0c;这也是为什么不少摄影爱好者会优先使用相机提供原始…

天梯赛 喊山 bfs

喊山&#xff0c;是人双手围在嘴边成喇叭状&#xff0c;对着远方高山发出“喂—喂喂—喂喂喂……”的呼唤。呼唤声通过空气的传递&#xff0c;回荡于深谷之间&#xff0c;传送到人们耳中&#xff0c;发出约定俗成的“讯号”&#xff0c;达到声讯传递交流的目的。原来它是彝族先…

ASP.NET Core on K8S深入学习(10)K8S包管理器Helm-Part 1

本篇已加入《.NET Core on K8S学习实践系列文章索引》&#xff0c;可以点击查看更多容器化技术相关系列文章。关于HelmWhy Helm&#xff1f;虽然K8S能够很好地组织和编排容器&#xff0c;但是缺少一个更高层次的应用打包工具&#xff0c;而Helm就是专门干这个事的。通过Helm能够…

520 钻石争霸赛 7-8浪漫侧影(二叉树的遍历)

“侧影”就是从左侧或者右侧去观察物体所看到的内容。例如上图中男生的侧影是从他右侧看过去的样子&#xff0c;叫“右视图”&#xff1b;女生的侧影是从她左侧看过去的样子&#xff0c;叫“左视图”。 520 这个日子还在打比赛的你&#xff0c;也就抱着一棵二叉树左看看右看看…

依赖注入在 dotnet core 中实现与使用:2 使用 Extensions DependencyInjection

既然是依赖注入容器&#xff0c;必然会涉及到服务的注册&#xff0c;获取服务实例&#xff0c;管理作用域&#xff0c;服务注入这四个方面。服务注册涉及如何将我们的定义的服务注册到容器中。这通常是实际开发中使用容器的第一步&#xff0c;而容器本身通常是由框架来实例化的…

520 钻石争霸赛 7-6 矩阵列平移(循环)

给定一个 nn 的整数矩阵。对任一给定的正整数 k<n&#xff0c;我们将矩阵的偶数列的元素整体向下依次平移 1、……、k、1、……、k、…… 个位置&#xff0c;平移空出的位置用整数 x 补。你需要计算出结果矩阵的每一行元素的和。 输入格式&#xff1a; 输入第一行给出 3 个…

拿 C# 搞函数式编程 - 2

前一阵子在写 CPU&#xff0c;导致一直没有什么时间去做其他的事情&#xff0c;现在好不容易做完闲下来了&#xff0c;我又可以水文章了哈哈哈哈哈。有关 FP 的类型部分我打算放到明年再讲&#xff0c;因为现有的 C# 虽然有一个 pattern matching expressions&#xff0c;但是没…

520 钻石争霸赛 7-5 大勾股定理 (数学)

基本思路&#xff1a; 这道题暴力拿到14分并不难&#xff0c;根据题意模拟即可&#xff0c;具体代码在下面。 至于最后一个测试点超时的问题&#xff0c;现已解决&#xff0c;AC代码在第二部分哦~ 参考代码&#xff08;14分&#xff09;&#xff1a; #include<bits/stdc.h…

.NETer,如何用.NET Core 3.0武装自己?这样学效率提高10倍!

都2020了 你还不会.NET Core&#xff1f; 2019年&#xff0c;.NET Core 3.0横空出世&#xff0c;越来越多的开发者开始关注.NET Core&#xff0c;越来越多的互联网软件公司开始使用.NET Core&#xff0c;各大.NET招聘岗位要求中&#xff0c;也将.NET Core列为必备技能&#xff…

DataFrame的多dtype创建方法

在创建DataFrame的时候&#xff0c;只有有一个dtype类型。 若使用numpy数组的字典&#xff0c;就可以分别设置dtype类型了。 import numpy as np import pandas as pddata {Site:np.array([Google, Runoob, Wiki],dtypestr),Age:np.array([10, 12, 13], dtypefloat),Year:np.…

ASP.NET Core on K8S深入学习(10)K8S包管理器Helm-Part 2

本篇已加入《.NET Core on K8S学习实践系列文章索引》&#xff0c;可以点击查看更多容器化技术相关系列文章。上一篇 Part 1 中介绍了Helm的基本概念与基本使用&#xff0c;这一篇我们来自定义一个Chart玩玩。自定义一个Chart1 创建Chart首先&#xff0c;通过以下命令创建一个c…

使用Vistual Studio N年,推荐2个异常捕获的技巧

点击上方“dotNET全栈开发”&#xff0c;“设为星标”加“星标★”&#xff0c;每天11.50&#xff0c;好文必达全文约1600字&#xff0c;预计阅读时间3分钟这个n到底是多少年&#xff1f;宇宙第一开发IDE Visual Studio的调试功能非常强大&#xff0c;平常工作debug帮助我们解决…

LeetCode动态规划 斐波那契数

斐波那契数&#xff0c;通常用 F(n) 表示&#xff0c;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c;F(1) 1 F(n) F(n - 1) F(n - 2)&#xff0c;其中 n > 1 给你 n &a…