从壹开始微服务 [ DDD ] 之五 ║聚合:实体与值对象 (上)

前言

哈喽,老张是周四放松又开始了,这些天的工做真的是繁重,三个项目同时启动,没办法,只能在深夜写文章了,如今时间的周四凌晨,白天上班已经没有时间开始写文章了,但愿看到文章的小伙伴,能给个辛苦赞👍哈哈,固然看心情很随意。废话很少说,话说上次我们对DDD简单说明了下存在的意义,还有就是基于教学上下文的第一次定义,今天我们就继续说说DDD领域驱动设计中的聚合相关知识,聚合这一块比较多,我暂时决定用两到三篇文章来讲说,今天就主要说一下“实体和值对象”的相关概念,其实以前我在定计划的时候,感受这一块应该很好说,可是晚上吃完饭搜索资料的时候,发现真的好多人对实体理解的还好,可是对值对象真是各类不理解,甚至嗤之以鼻,这一点我感受是很差的,但愿个人读者不要只会说这个很差,那个不对,而是想,这个东西既然产生了,而且一直被你们说着,也有在使用的,确定有存在的意义,举个栗子,可能今天你们看完对值对象仍是蒙胧胧,多想一想,多跟着DDD的思想走,也许就好多了,思想真的很难改变,不过只要努力了就是成功了。git

好!我们仍是开篇一个小问题,给你们正好一个思考的时间:github

我们从壹大学的后台系统中,每一个学生都有本身的家庭住址,确定会有这样或那样的缘由,会变化,那咱们是如何设计 Student模型 和 Address 模型的呢,这里只是说代码实现上,数据库实际上是对应的。数据库

一、在Students实体中,添加家庭地址属性:省、市、县、街道;c#

二、新建家庭地址Address实体,在Student中引入地址外键;架构

三、新建 Students 、Address、StuAdd三个表,在Students中引入List<Address>,一对多;框架

这个就是咱们平时的思路,不管是第一种的一对一(一个学生一个家庭地址),仍是第三种的一对多(一个学生多个家庭地址),若是你对这个思路很熟悉,那就须要好好看看今天的文章了,由于上边的这种仍是面向数据库数据开发的,但愿下边的说明,能让你对DDD的思想有必定的体验。ide

 

零、今天要实现蓝色的部分

 

 1、实体 —— 惟一标识

实体对应的英语单词为Entity。提到实体,你可能立马就想到了代码中定义的实体类。在使用一些ORM框架时,好比Entity Framework,实体做为直接反映数据库表结构的对象,就更尤其重要。特别是当咱们使用EF Code First时,咱们首先要作的就是实体类的设计。在DDD中,实体做为领域建模的工具之一,也是十分重要的概念。工具

但DDD中的实体和咱们以往开发中定义的实体是同一个概念吗?
不彻底是。在以往未实施DDD的项目中,咱们习惯于将关注点放在数据上,而非领域上。这也就说明了为何咱们在软件开发过程当中会首先作数据库的设计,进而根据数据库表结构设计相应的实体对象,这样的实体对象是数据模型转换的结果。
在DDD中,实体做为一个领域概念,在设计实体时,咱们将从领域出发。学习

一、DDD中的实体是什么

许多对象不是由它们的属性来定义,而是经过一系列的连续性(continuity)和标识(identity)来从根本上定义的。只要一个对象在生命周期中可以保持连续性,而且独立于它的属性(即便这些属性对系统用户很是重要),那它就是一个实体。

对于实体Entity,实体核心是用惟一的标识符来定义,而不是经过属性来定义。即即便属性彻底相同也多是两个不一样的对象。同时实体自己有状态的,实体又演进的生命周期,实体自己会体现出相关的业务行为,业务行为会实体属性或状态形成影响和改变。测试

若是从值对象自己无状态,不可变,而且不分配具体的标识层面来看。那么值对象能够仅仅理解为实际的Entity对象的一个属性结合而已。该值对象附属在一个实际的实体对象上面。值对象自己不存在一个独立的生命周期,也通常不会产生独立的行为。

二、为何要使用实体

当咱们须要考虑一个对象的个性特征,或者要区分不一样对象的时候,咱们就须要一个实体这个领域概念,一个实体是一个惟一的东西,而且能够长时间至关长的一段时间内持续的变化,可是不管咱们作了多少变化,这个的实体对象可能也已经变化的不少了,可是由于他们都一个相同的身份标识,全部仍是同一个实体。很简单,就好像一个学生,不管手机号,姓名,年龄,邮箱,是否毕业等等,所有变化了,由于惟一标识的缘由,咱们就能够认为,变化先后的全部对象,都是同一个实体。随着对象的改变,咱们可能会一直跟踪变化过程,何时,什么人,发生了什么变化:就好比学生由于学习太好,学校研究经过,提早毕业,更新状态为已毕业等。
这个时候咱们发现了,实体的两大特性:
一、有惟一的标识,不受状态属性的影响。
二、可变性特征,状态信息一直能够变化。
 

2、定义一个实体

 在咱们以前的代码中,咱们定义了 Student 模型,咱们是在当前模型中,添加了惟一标识 
  public class Student
    {
        protected Student() { }
        public Student(Guid id, string name, string email, DateTime birthDate)
        {
            Id = id;
            Name = name;
            Email = email;
            BirthDate = birthDate;
        }

        public Guid Id { get; private set; }//模型的惟一标识
        public string Name { get; private set; }
        public string Email { get; private set; }
        public string Phone { get; private set; }
        public DateTime BirthDate { get; private set; }
    }

 

 

咱们平时用到的标识都是 Int 类型,优势是占位少,内存小等,固然有时候受到长度的影响,咱们就用 long,

一、惟一标识都是什么类型

  通常咱们都是会倾向于使用int类型,映射到数据库中的自增加int。它的优点是简单,惟一性由数据库保障,占用空间小,查询速度快。我以前也采用了很长时间,大部分时候很好用,不过偶尔会很头痛。因为实体标识须要等到插入数据库以后才建立出来,因此你在保存以前不可能知道标识值是多少,若是在保存以前须要拿到Id,惟一的方法是先插入数据库,获得Id之后,再执行另外的操做,换句话说,须要把原本是同一个事务中的操做分红多个事务执行。除了这个问题,还有多个数据库表合并的问题,若是两个分表都是自增,那确定须要单独再一个字段来作标识,劳民伤财。

后来我就用string字符串来设置主键,最大的问题就出现了,就是有时候会出现一致的状况,却是保存失败,而后用户反馈,当测试的时候,又好了,这种幽灵事件。因此我就决定使用 Guid 了。

  它的主要优点是生成Guid很是容易,不管是Js,C#仍是在数据库中,都能轻易的生成出来。另外,Guid的惟一性很强,基本不可能生成出两个相同的Guid。

  Guid类型的主要缺点是占用空间太大。另外实体标识通常映射到数据库的主键,而Sql Server会默认把主键设成汇集索引,因为Guid的不连续性,这可能致使大量的页拆分,形成大量碎片从而拖慢查询。一个解决办法是使用Sql Server来生成Guid,它能够生成连续的Guid值,但这又回到了老路,只有插入数据库你才知道具体的Id值,因此行不通。另外一个解决办法是把汇集索引移到其它列上,好比建立时间。若是你打算把汇集索引继续放到Guid标识列上,能够观察到碎片通常都在90%以上,写一个Sql脚本,定时在半夜整理一下碎片,也算一个勉强的办法。

  若是生成一个有意义的流水号来做为标识,这时候标识类型就是一个字符串。

  有些时候可能还要使用更复杂的组合标识,这通常须要建立一个值对象做为标识类型。

既然每一个实体都有一个标识,那么为全部实体建立一个基类就显得颇有用了,这个基类就是层超类型,它为全部领域实体提供基础服务。

 

二、建立领域核心类库,并添加实体

在领域驱动设计中,咱们会有一些核心的公共的核心内容,因此类库 Christ.Domain.Core 就是起到的这个做用,除了领域模型外,还有之后的事件、命令和通知等核心内容类。
由于实体属于领域模型内容,因此咱们新建一个 Models 文件夹,并在其新建 Entity.cs 文件
这个时候,若是你问我,为何要单单定义一个 Entity 基类,而不把 Id 放到每个实体中,嗯,那就是尚未命名领域设计中,基于业务的考虑,咱们平时都是直接用面向数据库数据的思想来考虑的,duang duang设计表结构,天然而然的想到每个表(实体模型)必须有一个Id,可是如今,咱们是基于业务考虑的,每个业务下边会有子领域,而后每一个子领域都是聚合的,经过一个聚合根来关联,把类似的功能或者根单独拿出来,这个就是实体基类 Entity 的做用,固然除了 Id 还会有一些方法,好比如下:
 
namespace Christ.Domain.Core.Models
{
    /// <summary>
    /// 定义领域实体基类
    /// </summary>
    public abstract class Entity
    {
        /// <summary>
        /// 惟一标识
        /// </summary>
        public Guid Id { get; protected set; }

        /// <summary>
        /// 重写方法 相等运算
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            var compareTo = obj as Entity;

            if (ReferenceEquals(this, compareTo)) return true;
            if (ReferenceEquals(null, compareTo)) return false;

            return Id.Equals(compareTo.Id);
        }
        /// <summary>
        /// 重写方法 实体比较 ==
        /// </summary>
        /// <param name="a">领域实体a</param>
        /// <param name="b">领域实体b</param>
        /// <returns></returns>
        public static bool operator ==(Entity a, Entity b)
        {
            if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
                return true;

            if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
                return false;

            return a.Equals(b);
        }
        /// <summary>
        /// 重写方法 实体比较 !=
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static bool operator !=(Entity a, Entity b)
        {
            return !(a == b);
        }
        /// <summary>
        /// 获取哈希
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return (GetType().GetHashCode() * 907) + Id.GetHashCode();
        }
        /// <summary>
        /// 输出领域对象的状态
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return GetType().Name + " [Id=" + Id + "]";
        }
       
    }
}

 

 

 

三、实体模型继承该Entity

 修改咱们的 Student 模型,继承 Entity,并把属性 Id 去掉。

 

这个时候,咱们就已经把实体说完了,其实很简单,咱们平时也都在用,总结来讲如下两点:
一、实体的2大特性:惟一标识、可变性特性;
二、经过业务的思惟,去思考为何定义 Entity 的做用,主要也是起到了一个聚合的目的。
那实体咱们如今已经理解了它的概念,做用,产生以及意义,剩下的还有一个是实体验证支持,这个之后再说到,说到了实体,与之对应的是值对象,那值对象又是什么呢?请往下看。
 

3、值对象 —— 不变性

前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发( 就是Entity ),本篇将介绍另外一个重要的构造块——值对象,它是聚合中的主要成分。在咱们以前的开发中,由于是基于数据库数据的,因此咱们基本都是经过数据表来创建模型,这就是数据建模,而后依赖的是数据库范式设计,这样咱们就把每个数据库表就对应一个实体模型,每个表字段就对应应该实体属性。

在看咱们文章开头的那个问题,咱们就经常用第一种方法,

    public class Student : Entity
    {
        protected Student() { }
        public Student(Guid id, string name, string email, DateTime birthDate)
        {
            Id = id;
            Name = name;
            Email = email;
            BirthDate = birthDate;
        }
        //public Guid Id { get; private set; }
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; private set; }
        /// <summary>
        /// 邮箱
        /// </summary>
        public string Email { get; private set; }
        /// <summary>
        /// 手机
        /// </summary>
        public string Phone { get; private set; }
        /// <summary>
        /// 生日
        /// </summary>
        public DateTime BirthDate { get; private set; }

        /// <summary>
        /// 省份
        /// </summary>
        public string Province { get; private set; }

        /// <summary>
        /// 城市
        /// </summary>
        public string City { get; private set; }

        /// <summary>
        /// 区县
        /// </summary>
        public string County { get; private set; }

        /// <summary>
        /// 街道
        /// </summary>
        public string Street { get; private set; }
     
    }

可是,为了考虑不应有的属性,好比家庭地址信息,不该该出如今学生student的业务模型中,咱们就拆开,用两个实体进行表示,而后引入外键,就是咱们第二种方法。

    public class Student : Entity
    {
        //.....其余属性

        /// <summary>
        /// 地址外键
        /// </summary>
        public Address Address { get; private set; }
     
    }


    /// <summary>
    /// 地址
    /// </summary>
    public class Address :Entity {/// <summary>
        /// 省份
        /// </summary>
        public string Province { get; private set; }
        /// <summary>
        /// 城市
        /// </summary>
        public string City { get; private set; }

    }
}

 能够看到,对于这样的简单场景,通常有两个选择,要么把属性放到外部的实体中,只建立一张表,要么创建两个实体,并相应的建立两张表。第一种方法的缺点是,所有属性值放到一块儿,没有了总体业务概念,不只没法表达业务语义,并且使用起来很是困难,同时将不少没必要要的业务知识泄露到调用端。第二种方法的问题是致使了没必要要的复杂性。

  更好的方法很简单,就是把以上两种方法结合起来。咱们经过把地址建模成值对象,而不是实体,而后把值对象的属性值嵌入外部员工实体的表中,这种映射方式被称为嵌入值模式。换句话说,你如今的数据库表采用上面的第一种方式定义,而你在c#代码中经过第二种方式使用,只是把实体改为值对象。这样作的好处是显而易见的,既将业务概念表达得清楚,并且数据库也没有变得复杂。

一、值对象的概念

 值对象虽然有时候和实体特别想象,看上边的学校家庭信息就可得知,可是它却有着本身独有的好处,值对象很常见:好比数字,字符串,日期时间,甚至一我的的信息,邮寄地址等等,固然还有更复杂的值对象,这些都是反映 通用语言 概念的值对象。
咱们应该尽可能使用值对象来建模,而不是实体对象,你可能很想不通,即便上边的学生的家庭地址信息,你必定要单放一个数据库表,构建实体模型,在设计的时候咱们应该也要更偏向做为一个值对象容器,而不是子实体容器,由于这样咱们能够对值对象很好的建立,测试,使用,优化和维护。
 
当你决定一个领域概念是不是一个值对象的时候,你须要考虑它是否有如下特性:
一、它描述了领域中的一个东西
二、能够做为一个不变量。
三、当它被改变时,能够用另外一个值对象替换。
四、能够和别的值对象进行相等性比较。
 
在值对象中,咱们不关心标识,只要咱们能肯定该值对象的属性值都同样,咱们就能够说这两个值对象是相同的,好比咱们说两个学生的家庭地址(省市县街道门排)是同样的,咱们就能够认为是同一个地址,这就是相等性比较。
若是学生在修改地址的时候,咱们不是仅仅的修改省,或者市,或者县,并且将整个值对象给覆盖,这个就是值对象的不变性和可替换性。
 

4、如何建立一个地址值对象

一、建立值对象基类

在 Christ3D.Domain.Core 类库下的Models文件夹中,新建 ValueObject.cs
namespace Christ3D.Domain.Core.Models
{
    /// <summary>
    /// 定义值对象基类 
    /// 注意没有惟一标识了
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public abstract class ValueObject<T> where T : ValueObject<T>
    {
        /// <summary>
        /// 重写方法 相等运算
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            var valueObject = obj as T;
            return !ReferenceEquals(valueObject, null) && EqualsCore(valueObject);
        }

        protected abstract bool EqualsCore(T other);
        /// <summary>
        /// 获取哈希
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return GetHashCodeCore();
        }

        protected abstract int GetHashCodeCore();
        /// <summary>
        /// 重写方法 实体比较 ==
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static bool operator ==(ValueObject<T> a, ValueObject<T> b)
        {
            if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
                return true;

            if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
                return false;

            return a.Equals(b);
        }
        /// <summary>
        /// 重写方法 实体比较 !=
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static bool operator !=(ValueObject<T> a, ValueObject<T> b)
        {
            return !(a == b);
        }

        /// <summary>
        /// 克隆副本
        /// </summary>
        public virtual T Clone()
        {
            return (T)MemberwiseClone();
        }
    }
}

 

二、在 Christ3D.Domain 类库下的Models文件夹中,新建 Address 值对象

namespace Christ3D.Domain.Models
{
    /// <summary>
    /// 地址
    /// </summary>
    [Owned]
    public class Address : ValueObject<Address>
    {
        /// <summary>
        /// 省份
        /// </summary>
        public string Province { get; private set; }

        /// <summary>
        /// 城市
        /// </summary>
        public string City { get; private set; }

        /// <summary>
        /// 区县
        /// </summary>
        public string County { get; private set; }

        /// <summary>
        /// 街道
        /// </summary>
        public string Street { get; private set; }


        public Address() { }
        public Address(string province, string city,
            string county, string street)
        {
            this.Province = province;
            this.City = city;
            this.County = county;
            this.Street = street;
        }

      

        protected override bool EqualsCore(Address other)
        {
            throw new NotImplementedException();
        }

        protected override int GetHashCodeCore()
        {
            throw new NotImplementedException();
        }
    }
}

 

至此,咱们的Address就具备了值的特征,咱们能够直接使用Address address = new Address("北京市", "北京市", "海淀区", "一路 ");)来表示一个具体的经过属性识别的不可变的位置概念。在DDD中,咱们称这个Address为值对象。

 

三、实体与值对象的区别:

  1. 实体拥有标识,而值对象没有。
  2. 相等性测试方式不一样。实体根据标识判等,而值对象根据内部全部属性值判等。
  3. 实体容许变化,值对象不容许变化。
  4. 持久化的映射方式不一样。实体采用单表继承、类表继承和具体表继承来映射类层次结构,而值对象使用嵌入值或序列化大对象方式映射。
 

5、结语(待续)

今天由于时间的问题暂时就说这么多吧,这里只是把 实体 和值对象的概念和使用说明了下,具体的好处和强大的优点尚未来得及说,下一篇文章,我会说继续说聚合的内容,包括实体验证等,这篇文章也须要慢慢的润润色,加油吧
 

6、Github & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 

相关文章
相关标签/搜索