概述

本文将介绍领域驱动设计(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框架或者关系型数据库所限制,导致领域模型不断被打乱,重构领域模型变得越来越四不像,最终又写回了传统的三层架构或者面向数据库建模。

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

总结

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

如何运用DDD - 实体的更多相关文章

  1. Lind.DDD~实体属性变更追踪器的实现

    回到目录 看着这个标题很复杂,大叔把它拆开说一下,实体属性-变更-追踪器,把它拆成三部分大家看起来就容易懂一些了,实体属性:领域实体里有自己的属性,属性有getter,setter块,用来返回和设置属 ...

  2. Lind.DDD敏捷领域驱动框架~介绍

    回到占占推荐博客索引 最近觉得自己的框架过于复杂,在实现开发使用中有些不爽,自己的朋友们也经常和我说,框架太麻烦了,要引用的类库太多:之前架构之所以这样设计,完全出于对职责分离和代码附复用的考虑,主要 ...

  3. 应用程序框架实战十六:DDD分层架构之值对象(介绍篇)

    前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持.本篇将介绍另一个重要的构造块——值对象,它是聚合中的主要成分. 如果说你已经在使用DDD分层架构,但你却从来没有使 ...

  4. DDD分层架构之值对象(介绍篇)

    DDD分层架构之值对象(介绍篇) 前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持.本篇将介绍另一个重要的构造块——值对象,它是聚合中的主要成分. 如果说你已经在使 ...

  5. 如何运用DDD - 领域服务

    目录 如何运用DDD - 领域服务 概述 什么是领域服务 从实际场景下手 更贴近现实 领域服务VS应用服务 扩展上面的需求 最常见的认证授权是领域服务吗 使用领域服务 不要过多的使用领域服务 不要将过 ...

  6. .net架构设计读书笔记--第三章 第9节 域模型实现(ImplementingDomain Model)

        我们长时间争论什么方案是实现域业务领域层架构的最佳方法.最后,我们用一个在线商店案例来说明,其中忽略了许多之前遇到的一些场景.在线商店对很多人来说更容易理解. 一.在线商店项目简介 1. 用例 ...

  7. 如何使用ABP进行软件开发(2) 领域驱动设计和三层架构的对比

    简述 上一篇简述了ABP框架中的一些基础理论,包括ABP前后端项目的分层结构,以及后端项目中涉及到的知识点,例如DTO,应用服务层,整洁架构,领域对象(如实体,聚合,值对象)等. 笔者也曾经提到,AB ...

  8. DDD 领域驱动设计-两个实体的碰撞火花

    上一篇:<DDD 领域驱动设计-领域模型中的用户设计?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 在 ...

  9. 应用程序框架实战十五:DDD分层架构之领域实体(验证篇)

    在应用程序框架实战十四:DDD分层架构之领域实体(基础篇)一文中,我介绍了领域实体的基础,包括标识.相等性比较.输出实体状态等.本文将介绍领域实体的一个核心内容——验证,它是应用程序健壮性的基石.为了 ...

随机推荐

  1. sqlite复制表

    (1)复制表,并把原表的 所有记录都复制到新表里. CREATE TABLE newTb AS SELECT * FROM oldTb (2)只复制表结构,不复制数据到新表里. 注:该语句无法复制关键 ...

  2. MySQL的基础与安装

    一.数据库概述 1.什么是数据库? 数据库(Database)是按照数据结构来组织.存储和管理数据的建立在计算机存储设备上的仓库. 2.数据库的主要特点: ⑴ 实现数据共享        数据共享包含 ...

  3. ElasticSearch中文分词器-IK分词器的使用

    IK分词器的使用 首先我们通过Postman发送GET请求查询分词效果 GET http://localhost:9200/_analyze { "text":"农业银行 ...

  4. Bash shell类型

    登录shell(需要密码) 正常通过某一个终端来登录,需要输入用户名和密码. 使用su - username    使用su -l username 非登录shell(不需要密码) su userna ...

  5. C++学习笔记4_new和delete

    1. 默认的new和delete操作符new和delete是和c里面的mlloc和free是一样的,在堆中创建空间.堆中创建的,都要自己释放.C中void test(){ int *p=(int *) ...

  6. nginx篇高级之优化整理

    优化思路: 一.修改连接数和工作线程让其支持更多的并发量. worker_processes 2;                    //与CPU核心数量一致 worker_connections ...

  7. NOIP模拟测试14

    考完19了再写14,我也是够咕的. 14的题很好,也充分暴露了我的问题. T1是个分析性质推结论的题 对于区间[L,R],不妨设a[L]!=a[R],那么两个端点对答案没有贡献,也就是[L+1,R], ...

  8. java.io.StreamCorruptedException: invalid stream header: 00000000

    Caused by: java.io.StreamCorruptedException: invalid stream header: 00000000 at java.io.ObjectInputS ...

  9. 零基础Linux入门之《Linux就该这么学》

    本书是由全国多名红帽架构师(RHCA)基于最新Linux系统共同编写的高质量Linux技术自学教程,极其适合用于Linux技术入门教程或讲课辅助教材,目前是国内最值得去读的Linux教材,也是最有价值 ...

  10. coco-stuff and thing

    Defining things and stuff. The literature provides definitions for several aspects of stuff and thin ...