本文共 7000 字,大约阅读时间需要 23 分钟。
在常规开发中(事务脚本),我们所说的实体只是一些数据库映射的字段,对象只不过是包含业务功能描述的集合而已,在DDD(领域驱动设计)中,实体(Entity)和值对象(Value Object)是基本元素之一,事务脚本中所说的对象概念大概就是领域模型中领域(Domain)的概念了,但并不是只是业务功能描述的集合而已,不针对功能实现,而是针对业务协作完成的一种流程,DDD中的实体是一种领域对象,区别实体和值对象的方法就是判断是否有唯一标示,而不是属性,即使属性完全相同也可能是两个不同的对象。同时实体本身有状态的,而且有自己的生命周期,实体本身会体现出相关的业务行为,业务行为会对实体属性或状态造成影响和改变。比如双胞胎假设所有的属性都一样,仍然是两个不同的人。从哲学角度讲,这正是实体的本来含义。它是除了所有属性之外还不足以表达的“那个”东西,不依赖其它而自存的东西。
如何区分实体和值对象,比如在城市社保系统中,参与社保人就是一个实体,在业务系统中,社保是一种概念,针对的是参与社保人,所以我们要在业务中来区分参与社保人,人有可能同名同姓,所以不能用名字来区分,这里的名字就是参与社保人的一个属性,所以我们用身份证号来区分参与社保人,这里的身份证号就是参与社保人的唯一标示,用来说明:实体是什么?实体是哪个?
还有有一种业务场景是这样,比如在全国社保统计系统中,统计各个城市的参与社保的比率,因为我们只要知道这个人是不是参与了城市社保?而并不需要知道他是哪个人,所以这里面的参与社保人就是一个值对象,只是用来说明:值对象是什么?
可以看出区分实体和值对象只是在特定的业务场景下,同一种特定对象可能会有不同的方式看待,这里面就是一个边界的问题,而且特定的业务场景中的实体是有自己的状态和生命周期,这和值对象也有明显的区分。
道可道,非常道。名可名,非常名。 无名天地之始,有名万物之母。故常无欲以观其妙; 常有欲以观其徼(jiào)。 此两者同出而异名,同谓之玄,玄之又玄,众妙之门。 --《道德经》
上面这段话出自老子的道德经的开篇,简单说下前两段话的意思:道似乎有具体的定义,但总不是我们所想象出的定义,名取出了一个名,但不一定我们会一直使用。下面几段就是对有(名)和无(道)的辩证关系,最后得出:“无”是天地的来处,“有”是衍生万物的结果,这两者之间,同出为意义不一样,同样好似玄妙务必,无穷无尽,切是研究一切的门经。
为什么会引用道德经?其实在我看来,老子不做软件开发真是太亏了(哈哈),什么是实体和对象?可能每个人都有自己的解读,就像上面讨论的实体和值对象,其实某种意义上来说应该是领域对象和对象属性,这里说对象属性也并不是准确,就比如项目中我们使用的属性字典,并不是任何一种对象的属性,只是一个特定的值,不依附于任何对象。实体虽然称作实体,其实是一个对象,值对象虽然称作对象,但其实只是一个特定值,意义就像”道可道,非常道,名可名,非常名“一样,
老子探讨的有无关系,其实在软件编程中就是实体和对象的关系,“有”可以看做是“实体”,“无”可以看做是“对象”,无衍生出有,有体现出无,就像实体和对象之间的关系,实体是不是对象?对象是不是实体?实体和对象到底什么?这其实只是存在一个边界问题。正如DDD中,实体即是领域对象,对象即是实体模型,我们生活中常常讨论:“是先有的鸡蛋?还是先有的鸡?”最后都没有得出一个准确的结果,如果老子来回答这个问题,就六个字:“鸡生蛋,蛋生鸡”,至于解释,老子挥一挥衣袖,骑上青牛远去-“自己去琢磨吧”。
故常无欲以观其妙,常有欲以观其徼(jiào)。 --《道德经》
这段话我觉得是道德经开篇的精髓,你可能从字面上可以体会得到一些内容,这其实一种态度,一种生活态度,一种编程态度。
“故常无欲以观其妙”,这句话在我们的现实生活中可以很好的去解读,世间万物是如此的大,我们还有很多的事物没有去认知,所以我们就会抱着征服的欲望去探寻,看到美好的事物就想去掠夺占有,就像当你偶然发现一朵非常漂亮的鲜花,很多人不会停留下去欣赏它,而是去采摘它,然后据为己有,生活中的例子比比皆是,就像文章会犯错一样。
老子的所提倡的就是我们应该保持“无欲”的心态去看待事件万物,去观察,去体会它的玄妙,上面所说的掠夺、占有,就不是“观”所蕴含的意义了。我们在做项目中,项目前期需求还没有确定好就去开发,到最后弄得进退两难,在DDD中,业务需求是很重要的一环,我们应该花更多的时间去了解它、体会它、确定它,而不是想当然的了解后就去开发项目,这也是建模专家所必备的基本条件。
“常有欲以观其徼”,这句话的中的精髓就一个字“徼”,徼翻译为边界的意思,“有欲”在现实生活中可以指一些有名望、有地位、有财富的人,这些人当拥有了一些常人所不能拥有的东西后,并不懂得收敛和满足,反而使自己的欲望心更大,想得到更大的满足,得到后还想得到,没有一个界限,最后的下场一般都是很惨,就像和珅贪得无厌一样。
老子所提倡的就是我们在“有欲”之后,要观察、体会一个界限,要使自己的“欲”控制在这个界限中,水满则溢就是这个道理。就像在DDD中,实体和值对象边界的确定一样。
含德之厚,比于赤子。
专气致柔,能如婴儿乎?为天下豁,常德不离,复归于婴儿。--《道德经》老子在道德经中多次提到有关婴儿的话题,就像上面几句,总是拿一些事物和婴儿进行比较,难道说老子喜欢婴儿?准确的应该说,老子推崇婴儿的那种状态,何种状态?无欲无求、回归自然、保持天性。。。
人的进化史进行了千百万年,从最初的简单生存原则,发展到现在复杂的人世关系,越进化越复杂,导致我们现在越活越累。新出生的婴儿没有任何外界的掺杂,是如此的纯净,正如一碗清水一般,但随着成长,慢慢的接触外界事物,清水也会被染成五颜六色,而失去了本来固有的一些东西,这也就是为什么老子推崇婴儿的原因。有时候我们离开喧嚣的城市,置身于宁静的山坳,你会发现身心是如此的舒畅,其实这才是我们所固有的东西,只是处在乱世中,把那一抹清明掩盖罢了。
什么是初始实体和演化实体?这只不过是我自己定义的,这里面的实体也可以看做是对象,只是在DDD中称作为实体,如上面所说,婴儿就像初始实体一样,长大后的我们就是演化实体,初始实体只有一种状态,也就是一种原始状态或者称作是无状态,特定的场景下初始实体只有一个抽象出来的对象,但是演化实体有很多种,但都是从初始实体演化出来的,所以称为演化实体,有直接的关系也有间接的关系,如果把婴儿看做是初始实体,长大后的我们是演化实体,但演化实体并不只有长大后我们,汽车、成绩单、衣服等等一些与我们相关的事物都可以称为演化实体,但长大后的我们只是和初始实体有直接关系,其他的和初始实体都是间接关系,这个特定的场景就是人类进化史。
当然有人看到这可能有些想法,认为你这说的什么乱七八糟的东西,没有一点实际的意义。我的意思并不是说明初始实体和演化实体是个什么东西,而是说在我们做项目的过程中要找到那个“初始实体”,比如物流业务系统场景,在这个系统中哪个是初始实体?调度?账单?扫描?都不是,准确的说应该是运单,因为所有的业务操作都是围绕它来展开,或者是由它演生而来,虽然初始实体我们找到了,但是要仔细的揣摩它,确定是出生的婴儿还是长大后的我们?
聚合(Aggregate)和聚合根(Aggregate Root)是DDD中的重要概念,什么是聚合?它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成,聚合定义了一组具有内聚关系的相关对象的集合,我们把聚合看作是一个修改数据的单元。如果把人类社会看做是领域模型,聚合看做是一个国家,国家中的人是一个实体,那这个国家的人就是一个聚合根,但是每个国家的人都是人,只不过肤色、语言、习俗会有些不同,可以把人看做这个场景中的初始实体,也就是初始聚合根,而不是国家的人。
DTO(Data Transfer Object)数据传输对象,注意关键字“数据”两个字,并不是对象传输对象(Object Transfer Object),所以只是传输数据,并不包含领域业务处理,虽然用途只是传输数据,但本身其实也是对象,完成与领域对象之间的转换,就像上面说的值对象一样,某种意义上DTO可以看做是值对象的集合,只不过是和领域对象之间的映射,不包含任何的业务逻辑。
为什么要使用DTO?主要原因是隔离Domain Model,使改动领域模型而不影响UI,还有就是保持领域模型的安全,不暴露业务逻辑。还有就是在分布式模式下,不同的场景使用相同的数据结构有不同的需求,而我们又不得不做一些数据转化,这是很繁琐的,如果我们在项目初期,做DTO的分析,这样我们就会省很多的事,而且还不会影响整个项目的业务流程,也方便以后对项目进行扩展。
下面我们虚拟一个简单“文章”领域模型:
1 public class Article : IEntity 2 { 3 public Article() 4 { 5 this.Id = Guid.NewGuid(); 6 } 7 public string Title { get; set; } 8 public string Content { get; set; } 9 public string Author { get; set; }10 public DateTime PostTime { get; set; }11 public string Remark { get; set; }12 #region IEntity Members13 ///14 /// 读取或设置文章的编号 15 /// 16 public Guid Id { get; set; }17 #endregion18 }
文章领域模型对应DTO:
1 public class ArticleDTO 2 { 3 ///4 /// 文章唯一编码 5 /// 6 public string ArticleID { get; set; } 7 ///8 /// 文章标题 9 /// 10 public string Title { get; set; }11 ///12 /// 文章摘要13 /// 14 public string Summary { get; set; }15 ///16 /// 文章内容17 /// 18 public string Content { get; set; }19 ///20 /// 文章作者21 /// 22 public string Author { get; set; }23 ///24 /// 文章发表日期25 /// 26 public DateTime PostTime { get; set; }27 ///28 /// 文章发表年份29 /// 30 public int PostYear { get; set; }31 ///32 /// 文章备注33 /// 34 public string Remark { get; set; }35 }
从上面ArticleDTO中可以看到多了两个属性:Summary(文章摘要)和PostYear(文章发表年份),这就是我们在特定的业务场景中需要的,并不会影响到领域模型,如何实现领域模型和DTO之间的转换?我们可以使用AutoMapper可以很方便的对他们进行转换,一个强大的Object-Object Mapping工具。
工具-库程序包管理器-程序包管理控制平台,输入“Install-Package AutoMapper”命令,就可以把AutoMapper添加到项目中,有关AutoMapper相关文档可以参考:,这边简单演示下Article到ArticleDTO之间的转换。
1 static void Main(string[] args) 2 { 3 Article article = new Article 4 { 5 Title = "漫谈实体、对象、DTO及AutoMapper的使用", 6 Content = "实体(Entity)、对象(Object)、DTO(Data Transfer Object)数据传输对象,老生常谈话题,简单的概念,换个角度你会发现更多的东西。个人拙见,勿喜请喷。", 7 Author = "xishuai", 8 PostTime = DateTime.Now, 9 Remark = "文章备注"10 };11 //配置AutoMapper 12 AutoMapper.Mapper.Initialize(cfg =>13 {14 cfg.CreateMap()//创建映射15 .ForMember(dest => dest.ArticleID, opt => opt.MapFrom(src => src.Id))//指定映射规则16 .ForMember(dest => dest.Summary, opt => opt.MapFrom(src => src.Content.Substring(0, 10)))//指定映射规则17 .ForMember(dest => dest.PostYear, opt => opt.MapFrom(src => src.PostTime.Year))//指定映射规则18 .ForMember(dest => dest.Remark, opt => opt.Ignore());//指定映射规则 忽视没有的属性19 });20 21 //调用映射22 ArticleDTO form = AutoMapper.Mapper.Map (article);23 }
转换效果:
其实这篇文章原本只是想简单写下DTO及AutoMapper的用法,但是不知怎的写着写着就写跑偏了,现在回过头看也不知道自己写了些什么东西,正如张三丰教张无忌太极剑法,问他记得了多少,最后什么都没记得。
随着DDD(领域驱动设计)的学习和对道德经的感悟,就会发觉:此两者同出而异名,同谓之玄,玄之又玄,众妙之门。
本文转自田园里的蟋蟀博客园博客,原文链接:http://www.cnblogs.com/xishuai/p/3691787.html,如需转载请自行联系原作者