如何运用DDD - 实体

如何运用DDD - 实体

概述

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

何为实体

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

  • 实体(Entity,又称为Reference Object) 不少对象不是经过他们的属性定义的,而是经过一连串的连续事件和标识定义的。
  • 主要由标识定义的对象被称为ENTITY。

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

似曾相识

来想一下,咱们在以传统的设计思路和开发过程当中,咱们会在什么状况下为一个对象赋予一个ID呢?给它赋予这个ID的做用呢?通常来讲咱们的目的无非就是 一、为了区分本对象,若是是在数据库中,那就是为了区分本条数据和另一条数据,而这个ID也每每做为主键而存在 二、加个索引吧,来提高关联查找速度。因此咱们若是将数据库中的表映射到咱们的代码中以类的形式呈现的时候,它可能就是这个样子:c#

//旅行的行程
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框架或者关系型数据库所限制,致使领域模型不断被打乱,重构领域模型变得愈来愈四不像,最终又写回了传统的三层架构或者面向数据库建模。

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

总结

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

相关文章
相关标签/搜索