如何运用领域驱动设计 - 实体

原文:如何运用领域驱动设计 - 实体

目录

  • 概述
  • 何为实体
  • 似曾相识
    • 你确定它真的需要ID吗
  • 运用实体
    • 结合值对象
    • 为实体赋予它的行为
    • 尝试转移一部分行为给值对象
    • 愿景是美好的 现实是残酷的
  • 总结

概述

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

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

总结

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

原文地址:https://www.cnblogs.com/lonelyxmas/p/12065792.html

时间: 2024-11-11 02:17:27

如何运用领域驱动设计 - 实体的相关文章

领域驱动设计 - 在你开始之前清理一下你的观念

-------------------------------------之前园子里面有人翻译过,在家闲的没事又整理了一遍. Start Developing a New Application 开始一个新的应用程序 What we traditionally do when we start a business application? We read the spec and find the functionalities. We break down tasks. In most of

如何运用领域驱动设计 - 聚合

原文:如何运用领域驱动设计 - 聚合 目录 概述 何为聚合 演化案例 发现实体关系 开始划分边界吧 选取一个聚合根 通过聚合根保护你的内部对象 聚合的一些特性 通过ID引用 聚合真的是不变的吗 小的聚合 一致性 总结 概述 在前几篇的博文中,我们已经学习到了如何运用实体和值对象.随着我们所在领域的不断深入,领域模型变得逐渐清晰,我们已经建立了足够丰富的实体和值对象.但随着实体和值对象的数量逐渐增多,它们之间的关系也显得越来越复杂:实体A与实体B存在一对一的关系,实体B又与实体C存在一对多的关系.

DDD领域驱动设计之聚合、实体、值对象

关于具体需求,请看前面的博文:DDD领域驱动设计实践篇之如何提取模型,下面是具体的实体.聚合.值对象的代码,不想多说什么是实体.聚合等概念,相信理论的东西大家已经知晓了.本人对DDD表示好奇,没有在真正项目实践过,甚至也没有看过真正的DDD实践的项目源码,处于极度纠结状态,甚至无法自拔,所以告诫DDD爱好者们,如果要在项目里面实践DDD,除非你对实体建模和领域职责非常了解(很多时候会纠结一些逻辑放哪里好,属于设计问题)以及你的团队水平都比较高认同DDD,否则请慎重...勿喷! 代码在后,请先看D

学习领域驱动设计(三)之实体

前一篇文章:"学习领域驱动设计(二)之上下文映射图及架构"给大家主要介绍了上下文映射图的概念,以及粗略的简介了在领域驱动设计中主要使用到架构知识,而这篇文章主要来学习在领域驱动中实体的作用. 当我们在考虑一个对象的个性特征,或者需要区分不同的对象时,我们需要引入实体的概念. 一个实体是一个唯一的东西,并且可以在相当长的时间内持续发生变化.我们可以对实体进行多次修改,故实体对象可能和它先前的状态不大相同.但是,由于它们拥有相同的身份标识(Identity),它们依然是同一个实体. 唯一的

我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践

写在前面 插一句:本人超爱落网-<平凡的世界>这一期,分享给大家. 阅读目录: 关于DDD 前期分析 框架搭建 代码实现 开源-发布 后记 第一次听你,清风吹送,田野短笛:第一次看你,半弯新湖,鱼跃翠堤:第一次念你,燕飞巢冷,释怀记忆:第一次梦你,云翔海岛,轮渡迤逦:第一次认你,怨江别续,草桥知己:第一次怕你,命悬一线,遗憾禁忌:第一次悟你,千年菩提,生死一起. 人生有很多的第一次:小时候第一次牙牙学语.第一次学蹒跚学步...长大后第一次上课.第一次逃课.第一次骑自行车.第一次懂事.第一次和喜

DDD领域驱动设计基本理论知识总结

领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计),简称Evans DDD.领域驱动设计分为两个阶段: 以一种领域专家.设计人员.开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型:由领域模型驱动软件设计,用代码来实现该领域模型:

(转载)浅谈我对DDD领域驱动设计的理解

原文地址:http://www.cnblogs.com/netfocus/p/5548025.html 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品.所以,自然而然就想到要做一个普通电商系统,用于实现在线销售自己企业产品的目的. 再比如,我是一家互联网公司,公司有很多系统对外提供服务,面向很多客户端设备.但是最近由于各种原因,导致服务经常出故

领域驱动设计的面向服务架构

[.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店 一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这对于一些刚刚接触领域驱动设计的朋友可能会非常迷茫,从而觉得领域驱动设计很难,很复杂,因为学习中要消化一个整个案例的知识,这样未免很多人消化不了就打退堂鼓,就不继续研究下去了,所以这样也不利于DDD的推广.然而本系列

DDD领域驱动设计之领域服务

1.DDD领域驱动设计实践篇之如何提取模型 2.DDD领域驱动设计之聚合.实体.值对象 3.DDD领域驱动设计之领域基础设施层 什么是领域服务,DDD书中是说,有些类或者方法,放实体A也不好,放实体B也不好,因为很可能会涉及多个实体或者聚合的交互(也可能是多个相同类型的实体),此时就应该吧这些代码放到领域服务中,领域服务其实就跟传统三层的BLL很相似,只有方法没有属性,也就没有状态,而且最好是用动词命名,service为后缀,但是真正到了实践的时候,很多时候是很难区分是领域实体本身实现还是用领域