哈喽,老张是周四放松又开始了,这些天的工做真的是繁重,三个项目同时启动,没办法,只能在深夜写文章了,如今时间的周四凌晨,白天上班已经没有时间开始写文章了,但愿看到文章的小伙伴,能给个辛苦赞👍哈哈,固然看心情很随意。废话很少说,话说上次我们对DDD简单说明了下存在的意义,还有就是基于教学上下文的第一次定义,今天我们就继续说说DDD领域驱动设计中的聚合相关知识,聚合这一块比较多,我暂时决定用两到三篇文章来讲说,今天就主要说一下“实体和值对象”的相关概念,其实以前我在定计划的时候,感受这一块应该很好说,可是晚上吃完饭搜索资料的时候,发现真的好多人对实体理解的还好,可是对值对象真是各类不理解,甚至嗤之以鼻,这一点我感受是很差的,但愿个人读者不要只会说这个很差,那个不对,而是想,这个东西既然产生了,而且一直被你们说着,也有在使用的,确定有存在的意义,举个栗子,可能今天你们看完对值对象仍是蒙胧胧,多想一想,多跟着DDD的思想走,也许就好多了,思想真的很难改变,不过只要努力了就是成功了。git
好!我们仍是开篇一个小问题,给你们正好一个思考的时间:github
我们从壹大学的后台系统中,每一个学生都有本身的家庭住址,确定会有这样或那样的缘由,会变化,那咱们是如何设计 Student模型 和 Address 模型的呢,这里只是说代码实现上,数据库实际上是对应的。数据库
一、在Students实体中,添加家庭地址属性:省、市、县、街道;c#
二、新建家庭地址Address实体,在Student中引入地址外键;架构
三、新建 Students 、Address、StuAdd三个表,在Students中引入List<Address>,一对多;框架
这个就是咱们平时的思路,不管是第一种的一对一(一个学生一个家庭地址),仍是第三种的一对多(一个学生多个家庭地址),若是你对这个思路很熟悉,那就须要好好看看今天的文章了,由于上边的这种仍是面向数据库数据开发的,但愿下边的说明,能让你对DDD的思想有必定的体验。ide
实体对应的英语单词为Entity。提到实体,你可能立马就想到了代码中定义的实体类。在使用一些ORM框架时,好比Entity Framework,实体做为直接反映数据库表结构的对象,就更尤其重要。特别是当咱们使用EF Code First时,咱们首先要作的就是实体类的设计。在DDD中,实体做为领域建模的工具之一,也是十分重要的概念。工具
但DDD中的实体和咱们以往开发中定义的实体是同一个概念吗?
不彻底是。在以往未实施DDD的项目中,咱们习惯于将关注点放在数据上,而非领域上。这也就说明了为何咱们在软件开发过程当中会首先作数据库的设计,进而根据数据库表结构设计相应的实体对象,这样的实体对象是数据模型转换的结果。
在DDD中,实体做为一个领域概念,在设计实体时,咱们将从领域出发。学习
对于实体Entity,实体核心是用惟一的标识符来定义,而不是经过属性来定义。即即便属性彻底相同也多是两个不一样的对象。同时实体自己有状态的,实体又演进的生命周期,实体自己会体现出相关的业务行为,业务行为会实体属性或状态形成影响和改变。测试
若是从值对象自己无状态,不可变,而且不分配具体的标识层面来看。那么值对象能够仅仅理解为实际的Entity对象的一个属性结合而已。该值对象附属在一个实际的实体对象上面。值对象自己不存在一个独立的生命周期,也通常不会产生独立的行为。
一、有惟一的标识,不受状态属性的影响。二、可变性特征,状态信息一直能够变化。
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脚本,定时在半夜整理一下碎片,也算一个勉强的办法。
若是生成一个有意义的流水号来做为标识,这时候标识类型就是一个字符串。
有些时候可能还要使用更复杂的组合标识,这通常须要建立一个值对象做为标识类型。
既然每一个实体都有一个标识,那么为全部实体建立一个基类就显得颇有用了,这个基类就是层超类型,它为全部领域实体提供基础服务。
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 + "]"; } } }
一、实体的2大特性:惟一标识、可变性特性;二、经过业务的思惟,去思考为何定义 Entity 的做用,主要也是起到了一个聚合的目的。
前面介绍了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#代码中经过第二种方式使用,只是把实体改为值对象。这样作的好处是显而易见的,既将业务概念表达得清楚,并且数据库也没有变得复杂。
一、它描述了领域中的一个东西二、能够做为一个不变量。三、当它被改变时,能够用另外一个值对象替换。四、能够和别的值对象进行相等性比较。
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(); } } }
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为值对象。