上一篇,我介绍了本身在DDD分层架构方面的一些感想,本文开始介绍领域层的实体,代码主要参考自《领域驱动设计C#2008实现》,另外参考了网上找到的一些示例代码。数据库
由标识来区分的对象称为实体。架构
实体的定义隐藏了几个信息:框架
从实体的定义能够发现,标识是实体的关键特征。关于标识,有几个值得思考的问题。ide
好比中国人都有身份证,身份证号码是惟一的,那么可能会有人使用身份证号做为实体标识。这看起来好像没什么问题,但身份证每隔N年就会换代,身份证号可能发生变化。这违反了标识不可变性和稳定性要求,因此不适合做为实体标识。函数
对于手工录入流水号做为实体标识的状况,要用户本身保证惟一性已经很困难,若是提供了修改标识的功能,将致使标识不稳定,若是不提供,用户录入错误就只能删除后从新输入,这就太不人道了。性能
经过程序自动生成一个有意义的流水号做为实体标识,而且不提供修改,这多是可行的,对于惟一性要求,程序和数据库能够保证,另外不容许修改,就能够保证稳定性。对于像订单号一类的场景可能有效。单元测试
能够看到,使用有意义的值做为标识有必定风险,而且难度比较大,为了简单和方便,生成一个无心义的惟一值做为标识更可行。测试
对于使用Sql Server的同窗,通常会倾向于使用int类型,映射到数据库中的自增加int。它的优点是简单,惟一性由数据库保障,占用空间小,查询速度快。我以前也采用了很长时间,大部分时候很好用,不过偶尔会很头痛。ui
因为实体标识须要等到插入数据库以后才建立出来,因此你在保存以前不可能知道标识值是多少,若是在保存以前须要拿到Id,惟一的方法是先插入数据库,获得Id之后,再执行另外的操做,换句话说,须要把原本是同一个事务中的操做分红多个事务执行。this
使用自增加int类型的第二个毛病是,若是须要合并同一个实体对应的多个数据表记录,悲剧就会发生。好比你如今把一个实体对应的记录水平分区到多个数据库的表中,因为Id是自增加的,每一个表都会从1开始自增,你要合并到一个表中,Id就会发生冲突。因此对于比较大点的项目,使用自增加int类型是有一些风险的。
对于比较小,且不是太复杂的项目,使用自增加int类型是个不错的选择,但若是你常常碰到上面提到的问题,说明你须要从新选择标识类型了。
要解决以上问题,最简单的方法是选择Guid做为标识类型。
它的主要优点是生成Guid很是容易,不管是Js,C#仍是在数据库中,都能轻易的生成出来。另外,Guid的惟一性很强,基本不可能生成出两个相同的Guid。
Guid类型的主要缺点是占用空间太大。另外实体标识通常映射到数据库的主键,而Sql Server会默认把主键设成汇集索引,因为Guid的不连续性,这可能致使大量的页拆分,形成大量碎片从而拖慢查询。一个解决办法是使用Sql Server来生成Guid,它能够生成连续的Guid值,但这又回到了老路,只有插入数据库你才知道具体的Id值,因此行不通。另外一个解决办法是把汇集索引移到其它列上,好比建立时间。若是你打算把汇集索引继续放到Guid标识列上,能够观察到碎片通常都在90%以上,写一个Sql脚本,定时在半夜整理一下碎片,也算一个勉强的办法。
若是生成一个有意义的流水号来做为标识,这时候标识类型就是一个字符串。
有些时候可能还要使用更复杂的组合标识,这通常须要建立一个值对象做为标识类型。
我目前通常都使用Guid做为标识类型,偶尔使用字符串类型。
对于须要更详细的了解实体标识,请参考《企业应用架构模式》标识域一节。
既然每一个实体都有一个标识,那么为全部实体建立一个基类就显得颇有用了,这个基类就是层超类型,它为全部领域实体提供基础服务。
为了下降依赖性,如今须要在本系列应用程序框架的VS解决方案中增长一个类库Util.Domains和单元测试项目Util.Domains.Tests,并使用解决方案文件夹进行分类,以下图所示。
各程序集的依赖关系以下图所示。
实体基类能够取名为EntityBase,它应该是一个抽象类,具备一个名为Id的属性。若是采用int做为标识类型,代码多是这样。
namespace Util.Domains { /// <summary> /// 领域实体 /// </summary> public abstract class EntityBase{ /// <summary> /// 初始化领域实体 /// </summary> /// <param name="id">标识</param> protected EntityBase( int id ) { Id = id; } /// <summary> /// 标识 /// </summary> public int Id { get; private set; } } }
观察上面的代码,这里要考虑的关键问题是Id的set属性是否应该公共出来。根据前面的介绍,实体标识应该是不可变的,若是把Id的set属性设为公开,那么任何人均可以随时很方便的修改它,从而破坏了封装性。
那么,把Id的set属性设成私有,外界确实没法修改它,设置Id的惟一方法是在建立这个实体时,从构造函数传进来。但这会致使哪些问题?先看看ORM,它须要将数据库中的Id列映射到实体的Id属性上,若是set被设为私有,还能映射成功吗。经过测试,通常的ORM都具有映射私有属性的能力,好比EF,因此这不是问题。再来看看表现层,好比Mvc,Mvc提供了一个模型绑定功能,能够把表现层的数据映射到实体的属性上,若是属性是私有的会如何?测试之后,发现只有包含public 的set属性才能够映射成功,甚至字段都不行。再测试Wpf的双向绑定,也基本如此。因此把Id的set属性设为私有,将致使实体在表现层没法直接使用,须要经过Dto或ViewModel进行中转。
因此你须要在封装性和易用性上做出权衡,若是你但愿更高的健壮性,那就把Id的set属性隐藏起来,不然直接把Id暴露出来,经过约定告诉你们不要在建立了实体以后修改Id的值。因为本系列准备演示Dto的用法,因此会把Id setter隐藏起来,并经过Dto来转换。若是你须要更方便,请删除Id setter上的private。
如今Id类型为int,若是要使用Guid类型的实体,咱们须要建立另外一个实体基类。
namespace Util.Domains { /// <summary> /// 领域实体 /// </summary> public abstract class EntityBase{ /// <summary> /// 初始化领域实体 /// </summary> /// <param name="id">标识</param> protected EntityBase( Guid id ) { Id = id; } /// <summary> /// 标识 /// </summary> public Guid Id { get; private set; } } }
它们的惟一变化是Id数据类型不一样,咱们能够把Id类型设为object,从而支持全部类型。
namespace Util.Domains { /// <summary> /// 领域实体 /// </summary> public abstract class EntityBase{ /// <summary> /// 初始化领域实体 /// </summary> /// <param name="id">标识</param> protected EntityBase( object id ) { Id = id; } /// <summary> /// 标识 /// </summary> public object Id { get; private set; } } }
但弱类型的object将致使装箱和拆箱,另外也不太易用,这时候是泛型准备登场的时候了。
namespace Util.Domains { /// <summary> /// 领域实体 /// </summary> /// <typeparam name="TKey">标识类型</typeparam> public abstract class EntityBase<TKey> { /// <summary> /// 初始化领域实体 /// </summary> /// <param name="id">标识</param> protected EntityBase( TKey id ) { Id = id; } /// <summary> /// 标识 /// </summary> [Required] public TKey Id { get; private set; } } }
将标识类型经过泛型参数TKey传进来,因为标识类型能够任意,因此不须要进行泛型约束。另外在Id上方加了一个Required特性,当Id为字符串或其它引用类型的时候,就能派上用场了。
下面要解决的问题是实体对象相等性比较,须要重写Equals,GetHashCode方法,另外须要重写==和!=两个操做符重载。
/// <summary> /// 相等运算 /// </summary> public override bool Equals( object entity ) { if ( entity == null ) return false; if ( !( entity is EntityBase<TKey> ) ) return false; return this == (EntityBase<TKey>)entity; } /// <summary> /// 获取哈希 /// </summary> public override int GetHashCode() { return Id.GetHashCode(); } /// <summary> /// 相等比较 /// </summary> /// <param name="entity1">领域实体1</param> /// <param name="entity2">领域实体2</param> public static bool operator ==( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) { if ( (object)entity1 == null && (object)entity2 == null ) return true; if ( (object)entity1 == null || (object)entity2 == null ) return false; if ( entity1.Id == null ) return false; if ( entity1.Id.Equals( default( TKey ) ) ) return false; return entity1.Id.Equals( entity2.Id ); } /// <summary> /// 不相等比较 /// </summary> /// <param name="entity1">领域实体1</param> /// <param name="entity2">领域实体2</param> public static bool operator !=( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) { return !( entity1 == entity2 ); }
在操做符==的代码中,有一句须要注意,entity1.Id.Equals( default( TKey ) ),好比,一个实体的标识为int类型,这个实体在刚建立的时候,Id默认为0,另外建立一个该类的实例,Id也为0,那么这两个实体是相等仍是不等?从逻辑上它们是不相等的,属于不一样的实体, 只是标识目前尚未建立,可能须要等到保存到数据库中才能产生。这有什么影响呢?当进行某些集合操做时,若是你发现操做N个实体,但只有一个实体操做成功,那颇有多是由于这些实体的标识是默认值,而你的相等比较没有识别出来,这一句代码可以解决这个问题。
考虑领域实体基类还能帮咱们干点什么,其实还不少,好比状态输出、初始化、验证、日志等。下面先来介绍一下状态输出。
当我在操做每一个实体的时候,我常常须要在日志中记录完整的实体状态,即实体全部属性名值对的列表。这样方便我在查找问题的时候,能够了解某个实体当时是个什么状况。
要输出实体的状态,最方便的方法是重写ToString,而后把实体状态列表返回回来。这样ToString方法将变得有意义,由于它输出一个实体的类名基本没什么用。
要输出实体的所有属性值,一个办法是经过反射在基类中进行,但这可能会形成一点性能降低,因为经过代码生成器能够轻松生成这个操做,因此我没有采用反射的方法。
/// <summary> /// 描述 /// </summary> private StringBuilder _description; /// <summary> /// 输出领域对象的状态 /// </summary> public override string ToString() { _description = new StringBuilder(); AddDescriptions(); return _description.ToString().TrimEnd().TrimEnd( ',' ); } /// <summary> /// 添加描述 /// </summary> protected virtual void AddDescriptions() { } /// <summary> /// 添加描述 /// </summary> protected void AddDescription( string description ) { if ( string.IsNullOrWhiteSpace( description ) ) return; _description.Append( description ); } /// <summary> /// 添加描述 /// </summary> protected void AddDescription<T>( string name, T value ) { if ( string.IsNullOrWhiteSpace( value.ToStr() ) ) return; _description.AppendFormat( "{0}:{1},", name, value ); }
在子类中须要重写AddDescriptions方法,并在该方法中调用AddDescription这个辅助方法来添加属性名值对的描述。
因为验证和日志等内容须要一些公共操做类提供帮助,因此放到后面几篇进行介绍。
为了使泛型的EntityBase<TKey>用起来更简单一点,我建立了一个EntityBase,它从泛型EntityBase<Guid>派生,这是由于我如今主要使用Guid做为标识类型。
namespace Util.Domains { /// <summary> /// 领域实体基类 /// </summary> public abstract class EntityBase : EntityBase<Guid> { /// <summary> /// 初始化领域实体 /// </summary> /// <param name="id">标识</param> protected EntityBase( Guid id ) : base( id ) { } } }
完整单元测试代码以下。
using System; namespace Util.Domains.Tests.Samples { /// <summary> /// 测试实体 /// </summary> public class Test : EntityBase { /// <summary> /// 初始化 /// </summary> public Test() : this( Guid.NewGuid() ) { } /// <summary> /// 初始化员工 /// </summary> /// <param name="id">员工编号</param> public Test( Guid id ) : base( id ) { } /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 添加描述 /// </summary> protected override void AddDescriptions() { AddDescription( "Id:"+ Id + "," ); AddDescription( "姓名", Name ); } } } using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using Util.Domains.Tests.Samples; namespace Util.Domains.Tests { /// <summary> /// 实体基类测试 /// </summary> [TestClass] public class EntityBaseTest { /// <summary> /// 测试实体1 /// </summary> private Test _test1; /// <summary> /// 测试实体2 /// </summary> private Test _test2; /// <summary> /// 测试初始化 /// </summary> [TestInitialize] public void TestInit() { _test1 = new Test(); _test2 = new Test(); } /// <summary> /// 经过构造方法设置标识 /// </summary> [TestMethod] public void TestId() { Guid id = Guid.NewGuid(); _test1 = new Test( id ); Assert.AreEqual( id, _test1.Id ); } /// <summary> /// 新建立的实体不相等 /// </summary> [TestMethod] public void TestNewEntityIsNotEquals() { Assert.IsFalse( _test1.Equals( _test2 ) ); Assert.IsFalse( _test1.Equals( null ) ); Assert.IsFalse( _test1 == _test2 ); Assert.IsFalse( _test1 == null ); Assert.IsFalse( null == _test2 ); Assert.IsTrue( _test1 != _test2 ); Assert.IsTrue( _test1 != null ); Assert.IsTrue( null != _test2 ); } /// <summary> /// 当两个实体的标识相同,则实体相同 /// </summary> [TestMethod] public void TestEntityEquals_IdEquals() { Guid id = Guid.NewGuid(); _test1 = new Test( id ); _test2 = new Test( id ); Assert.IsTrue( _test1.Equals( _test2 ) ); Assert.IsTrue( _test1 == _test2 ); Assert.IsFalse( _test1 != _test2 ); } /// <summary> /// 测试状态输出 /// </summary> [TestMethod] public void TestToString() { _test1 = new Test { Name = "a" }; Assert.AreEqual( string.Format( "Id:{0},姓名:a", _test1.Id ), _test1.ToString() ); } } }
完整EntityBase代码以下。
using System.ComponentModel.DataAnnotations; using System.Text; namespace Util.Domains { /// <summary> /// 领域实体 /// </summary> /// <typeparam name="TKey">标识类型</typeparam> public abstract class EntityBase<TKey> { #region 构造方法 /// <summary> /// 初始化领域实体 /// </summary> /// <param name="id">标识</param> protected EntityBase( TKey id ) { Id = id; } #endregion #region 字段 /// <summary> /// 描述 /// </summary> private StringBuilder _description; #endregion #region Id(标识) /// <summary> /// 标识 /// </summary> [Required] public TKey Id { get; private set; } #endregion #region Equals(相等运算) /// <summary> /// 相等运算 /// </summary> public override bool Equals( object entity ) { if ( entity == null ) return false; if ( !( entity is EntityBase<TKey> ) ) return false; return this == (EntityBase<TKey>)entity; } #endregion #region GetHashCode(获取哈希) /// <summary> /// 获取哈希 /// </summary> public override int GetHashCode() { return Id.GetHashCode(); } #endregion #region ==(相等比较) /// <summary> /// 相等比较 /// </summary> /// <param name="entity1">领域实体1</param> /// <param name="entity2">领域实体2</param> public static bool operator ==( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) { if ( (object)entity1 == null && (object)entity2 == null ) return true; if ( (object)entity1 == null || (object)entity2 == null ) return false; if ( entity1.Id == null ) return false; if ( entity1.Id.Equals( default( TKey ) ) ) return false; return entity1.Id.Equals( entity2.Id ); } #endregion #region !=(不相等比较) /// <summary> /// 不相等比较 /// </summary> /// <param name="entity1">领域实体1</param> /// <param name="entity2">领域实体2</param> public static bool operator !=( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) { return !( entity1 == entity2 ); } #endregion #region ToString(输出领域对象的状态) /// <summary> /// 输出领域对象的状态 /// </summary> public override string ToString() { _description = new StringBuilder(); AddDescriptions(); return _description.ToString().TrimEnd().TrimEnd( ',' ); } /// <summary> /// 添加描述 /// </summary> protected virtual void AddDescriptions() { } /// <summary> /// 添加描述 /// </summary> protected void AddDescription( string description ) { if ( string.IsNullOrWhiteSpace( description ) ) return; _description.Append( description ); } /// <summary> /// 添加描述 /// </summary> protected void AddDescription<T>( string name, T value ) { if ( string.IsNullOrWhiteSpace( value.ToStr() ) ) return; _description.AppendFormat( "{0}:{1},", name, value ); } #endregion } } using System; namespace Util.Domains { /// <summary> /// 领域实体基类 /// </summary> public abstract class EntityBase : EntityBase<Guid> { /// <summary> /// 初始化领域实体 /// </summary> /// <param name="id">标识</param> protected EntityBase( Guid id ) : base( id ) { } } }
为了完成实体基类的验证,我须要先提供两个公共操做类,即验证和自定义异常类,待把这两个类完成后,咱们再继续介绍实体基类在验证方面的支持。
.Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。若是发现代码中有BUG,请及时告知,我将迅速修复。
谢谢你们的持续关注,个人博客地址:http://www.cnblogs.com/xiadao521/
下载地址:http://files.cnblogs.com/xiadao521/Util.2014.11.17.1.rar