一、什么是实体

  由标识来区分的对象称为实体。

  实体的定义隐藏了几个信息:

  • 两个实体对象,只要它们的标识属性值相等,哪怕标识属性以外的所有属性值都不相等,这两个对象也认为是同一个实体,这意味着两个对象是同一实体在其生命周期内的不同阶段。
  • 为了能正确区分实体,标识必须唯一。
  • 实体的标识属性值是不可变的,标识属性以外的属性值是可变的。如果标识值不大稳定,偶尔会变化,那么就无法将该实体在生命周期内的所有变化关联在一起,这可能导致严重的问题。

  二、实体标识

  从实体的定义可以发现,标识是实体的关键特征。关于标识,有几个值得思考的问题。

  1、将什么选作标识

  比如中国人都有身份证,身份证号码是唯一的,那么可能会有人使用身份证号作为实体标识。这看起来好像没什么问题,但身份证每隔N年就会换代,身份证号可能发生变化。这违反了标识不可变性和稳定性要求,所以不适合作为实体标识。

  对于手工录入流水号作为实体标识的情况,要用户自己保证唯一性已经很困难,如果提供了修改标识的功能,将导致标识不稳定,如果不提供,用户录入错误就只能删除后重新输入,这就太不人道了。

  通过程序自动生成一个有意义的流水号作为实体标识,并且不提供修改,这可能是可行的,对于唯一性要求,程序和数据库可以保证,另外不允许修改,就可以保证稳定性。对于像订单号一类的场景可能有效。

  可以看到,使用有意义的值作为标识有一定风险,并且难度比较大,为了简单和方便,生成一个无意义的唯一值作为标识更可行。

  2、为标识选择什么类型

  对于使用Sql Server的同学,一般会倾向于使用int类型,映射到数据库中的自增长int。它的优势是简单,唯一性由数据库保障,占用空间小,查询速度快。我之前也采用了很长时间,大部分时候很好用,不过偶尔会很头痛。

  由于实体标识需要等到插入数据库之后才创建出来,所以你在保存之前不可能知道标识值是多少,如果在保存之前需要拿到Id,唯一的方法是先插入数据库,得到Id以后,再执行另外的操作,换句话说,需要把本来是同一个事务中的操作分成多个事务执行。

  使用自增长int类型的第二个毛病是,如果需要合并同一个实体对应的多个数据表记录,悲剧就会发生。比如你现在把一个实体对应的记录水平分区到多个数据库的表中,由于Id是自增长的,每个表都会从1开始自增,你要合并到一个表中,Id就会发生冲突。所以对于比较大点的项目,使用自增长int类型是有一些风险的。

  对于比较小,且不是太复杂的项目,使用自增长int类型是个不错的选择,但如果你经常碰到上面提到的问题,说明你需要重新选择标识类型了。

  要解决以上问题,最简单的方法是选择Guid作为标识类型。

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

  Guid类型的主要缺点是占用空间太大。另外实体标识一般映射到数据库的主键,而Sql Server会默认把主键设成聚集索引,由于Guid的不连续性,这可能导致大量的页拆分,造成大量碎片从而拖慢查询。一个解决办法是使用Sql Server来生成Guid,它可以生成连续的Guid值,但这又回到了老路,只有插入数据库你才知道具体的Id值,所以行不通。另一个解决办法是把聚集索引移到其它列上,比如创建时间。如果你打算把聚集索引继续放到Guid标识列上,可以观察到碎片一般都在90%以上,写一个Sql脚本,定时在半夜整理一下碎片,也算一个勉强的办法。(针对此种情况,可以参考abp框架中生成guid的类库方法,其机制是可以预先生成guid,同时生成的guid是递增的。

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

  有些时候可能还要使用更复杂的组合标识,这一般需要创建一个值对象作为标识类型。

  我目前一般都使用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 ) {
}
}
} EntityBase

转载链接:http://www.cnblogs.com/xiadao521/p/4104190.html

应用程序框架(一):DDD分层架构:领域实体(基础篇)的更多相关文章

  1. 应用程序框架实战十八:DDD分层架构之聚合

    前面已经介绍了DDD分层架构的实体和值对象,本文将介绍聚合以及与其高度相关的并发主题. 我在之前已经说过,初学者第一步需要将业务逻辑尽量放到实体或值对象中,给实体“充血”,这样可以让业务逻辑高度内聚, ...

  2. 应用程序框架实战十六:DDD分层架构之值对象(介绍篇)

    前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持.本篇将介绍另一个重要的构造块——值对象,它是聚合中的主要成分. 如果说你已经在使用DDD分层架构,但你却从来没有使 ...

  3. DDD分层架构之聚合

    DDD分层架构之聚合 前面已经介绍了DDD分层架构的实体和值对象,本文将介绍聚合以及与其高度相关的并发主题. 我在之前已经说过,初学者第一步需要将业务逻辑尽量放到实体或值对象中,给实体“充血”,这样可 ...

  4. DDD分层架构之值对象(介绍篇)

    DDD分层架构之值对象(介绍篇) 前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持.本篇将介绍另一个重要的构造块——值对象,它是聚合中的主要成分. 如果说你已经在使 ...

  5. 应用程序框架实战十五:DDD分层架构之领域实体(验证篇)

    在应用程序框架实战十四:DDD分层架构之领域实体(基础篇)一文中,我介绍了领域实体的基础,包括标识.相等性比较.输出实体状态等.本文将介绍领域实体的一个核心内容——验证,它是应用程序健壮性的基石.为了 ...

  6. 应用程序框架实战十四:DDD分层架构之领域实体(基础篇)

    上一篇,我介绍了自己在DDD分层架构方面的一些感想,本文开始介绍领域层的实体,代码主要参考自<领域驱动设计C#2008实现>,另外参考了网上找到的一些示例代码. 什么是实体 由标识来区分的 ...

  7. 应用程序框架实战十七:DDD分层架构之值对象(层超类型篇)

    上一篇介绍了值对象的基本概念,得到了一些朋友的支持,另外也有一些朋友提出了不同意见.这其实是很自然的事情,设计本来就充满了各种可能性,没有绝对正确的做法,只有更好的实践.但是设计与实践的好与坏,对于不 ...

  8. 应用程序框架实战十三:DDD分层架构之我见

    前面介绍了应用程序框架的一个重要组成部分——公共操作类,并提供了一个数据类型转换公共操作类作为示例进行演示.下面准备介绍应用程序框架的另一个重要组成部分,即体系架构支持.你不一定要使用DDD这样的架构 ...

  9. 应用程序框架实战十三:DDD分层架构之我见(转)

    前面介绍了应用程序框架的一个重要组成部分——公共操作类,并提供了一个数据类型转换公共操作类作为示例进行演示.下面准备介绍应用程序框架的另一个重要组成部分,即体系架构支持.你不一定要使用DDD这样的架构 ...

  10. DDD分层架构之领域实体(验证篇)

    DDD分层架构之领域实体(验证篇) 在应用程序框架实战十四:DDD分层架构之领域实体(基础篇)一文中,我介绍了领域实体的基础,包括标识.相等性比较.输出实体状态等.本文将介绍领域实体的一个核心内容—— ...

随机推荐

  1. 深度优先搜索 - 简单demo

    输入一个数n,输出 1 ~ n 的全排列,例如输入 3,全排列则为:123,132,213,231,312,321 一共六种. 这里采用深度优先搜索来解决这个问题: #include<stdio ...

  2. Android之开源项目汇总

    汇总一:http://www.trinea.cn/android/android-open-source-projects-view/ 汇总二:http://blog.csdn.net/liuhaom ...

  3. mysql 练习 和链接 pymysql 练习题

    python操作数据库 1. 查询student表的所有记录 2. 查询student表的第2条到第4条记录 3. 查询所有学生的学号(id).姓名(name)和报读课程(department)的信息 ...

  4. 导入不用的css文件及在不同设备显示不用的html页面

    当一个页面对应有多个css样式文件时,我们可以根据地址栏的参数值而导入不同的css文件: function getCss() { var linkNode = document.createEleme ...

  5. chrome inspect 远程调试H5

    chrome://inspect/#devices 一个内置于chrome的远程调试指令,满足远程调试的几个必须条件 1,能够访问https://chrome-devtools-frontend.ap ...

  6. leetcode-179-Largest Number(理解规则,自定义cmp函数进行排序)

    题目描述: 给定一组非负整数,重新排列它们的顺序使之组成一个最大的整数. 示例 1: 输入: [10,2] 输出: 210 示例 2: 输入: [3,30,34,5,9] 输出: 9534330 说明 ...

  7. leetcode-165-比较版本号

    题目描述: 比较两个版本号 version1 和 version2.如果 version1 > version2 返回 1,如果 version1 < version2 返回 -1, 除此 ...

  8. ES6-Iterator & for...of循环

    依赖文件地址 :https://github.com/chanceLe/ES6-Basic-Syntax/tree/master/js <!DOCTYPE html> <html&g ...

  9. golang的并行快速排序

    .nums[]作为core,将nums中大于core的元素放入greater,将不大于core的元素放入less 当nums长度为1时往ch中写入此元素 .分别对less和greater进行1操作(并 ...

  10. Python中文分词 jieba

    三种分词模式与一个参数 以下代码主要来自于jieba的github,你可以在github下载该源码 import jieba seg_list = jieba.cut("我来到北京清华大学& ...