前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持。本篇将介绍另外一个重要的构造块——值对象,它是聚合中的主要成分。数据库
若是说你已经在使用DDD分层架构,但你却历来没有使用过值对象,这绝不奇怪,由于多年来养成的数据建模思惟已经紧紧把你禁锢,以至于你在使用面向对象方式进行开发时,仍是以数据为中心。c#
当咱们完成了基本的需求分析之后,若是说须要进行设计,那么你能想到的就是数据库表及表关系的设计,这就是数据建模。数据建模的主要依据是数据库范式设计,根据要求严格程度的递增分为第N范式,基本的要求是把每一个标量属性值用单独的一列来存储,每一个非键属性必须彻底依赖于键属性。数据库范式设计的目标是消除存储在多个位置上的冗余数据,以避免致使更新异常。为了达到这个目的,须要进行不断的表拆分,直到每一个表都只表示一个单一的概念。这能够认为是SRP(单一职责原则)在表上的应用,从而使表中的数据产生更高的内聚性。这从数据库的角度看多是不错的,但对于面向对象开发却不见得是个好事。安全
每个表称为一个数据库实体。当你完成了表设计之后,很天然的把数据库实体与DDD实体等同起来,这产生了一个直观的映射,因此每一个表在你的系统中都是一个实体。受这个根深蒂固的开发模式影响,你与值对象无缘相见。架构
值对象不只在概念上提供强大的帮助,并且在技术上,特别是持久化方面可以大幅简化系统设计,后面我将逐步介绍聚合与值对象是如何帮助你下降系统复杂性而脱困的。并发
经过对象属性值来识别的对象,它将多个相关属性组合为一个概念总体。框架
在值对象的概念中,隐含了以下信息:异步
看了上面的概念描述,可能并不能打动你。你会说“实体不就比值对象多一个标识,能复杂到哪去”。因为你使用实体一样能够对业务概念建模,因此是否使用值对象,对你来讲根本不重要。数据库设计
下面来看看使用值对象的其它好处。性能
值对象的一个做用是能够帮助优化性能。当一个值对象须要在多个地方使用时,能够共享同一个值对象。为了共享同一个值对象,你可使用工厂来建立单例模式的值对象实例,因为值对象是不可变的,因此能够安全的使用。测试
固然,你可能对使用值对象来提高性能也不感兴趣,你须要更实在的好处,不然就免谈。下面将介绍值对象的重型武器,它对你将产生空前的影响,甚至颠覆你平时的建模习惯和开发模式。
前面已经说过,你为了知足数据库规范化设计,建立大量的表,各个表之间关系错综复杂,并且你也意识到正是表的膨胀致使了系统复杂性的上升。若是可以减小表的数量,那么表之间的关系也会变得简单和清晰,有什么办法能够减小表的数量吗?答案就是值对象与逆范式设计。
首先来看一个简单状况。如今要为人力资源系统创建员工档案,咱们使用一个名为Employee的员工类来表示这个业务概念,除了名字之外,还要管理他的地址信息,咱们能够将地址信息直接放到员工实体上,数据库表结构与员工实体同样,代码以下所示。
/// <summary>
/// 员工 /// </summary>
public class Employee : EntityBase { /// <summary>
/// 姓名 /// </summary>
public string Name { get; set; } /// <summary>
/// 省份 /// </summary>
public string Province { get; set; } /// <summary>
/// 城市 /// </summary>
public string City { get; set; } /// <summary>
/// 区县 /// </summary>
public string County { get; set; } /// <summary>
/// 街道 /// </summary>
public string Street { get; set; } /// <summary>
/// 邮政编码 /// </summary>
public string Zip { get; set; } }
不过你的数据库规范化专业技能很是敏感,让你察觉到这几个地址属性都不彻底依赖于员工主键,因此你决定专门建一张地址表,再把地址表与员工表关联起来。
你的代码也做出相应调整以下。
/// <summary>
/// 员工 /// </summary>
public class Employee : EntityBase{ /// <summary>
/// 姓名 /// </summary>
public string Name { get; set; } /// <summary>
/// 地址编号 /// </summary>
public Guid AddressId { get; set; } /// <summary>
/// 地址 /// </summary>
public Address Address { get; set; } } /// <summary>
/// 地址 /// </summary>
public class Address : EntityBase { /// <summary>
/// 省份 /// </summary>
public string Province { get; set; } /// <summary>
/// 城市 /// </summary>
public string City { get; set; } /// <summary>
/// 区县 /// </summary>
public string County { get; set; } /// <summary>
/// 街道 /// </summary>
public string Street { get; set; } /// <summary>
/// 邮政编码 /// </summary>
public string Zip { get; set; } }
能够看到,对于这样的简单场景,通常有两个选择,要么把属性放到外部的实体中,只建立一张表,要么创建两个实体,并相应的建立两张表。第一种方法的问题是,一个总体业务概念被弱化成一堆零碎的属性值,不只没法表达业务语义,并且使用起来很是困难,同时将不少没必要要的业务知识泄露到调用端。第二种方法的问题是致使了没必要要的复杂性。
更好的方法很简单,就是把以上两种方法结合起来。咱们经过把地址建模成值对象,而不是实体,而后把值对象的属性值嵌入外部员工实体的表中,这种映射方式被称为嵌入值模式。换句话说,你如今的数据库表采用上面的第一种方式定义,而你在c#代码中经过第二种方式使用,只是把实体改为值对象。这样作的好处是显而易见的,既将业务概念表达得清楚,并且数据库也没有变得复杂,可谓鱼和熊掌兼得。
使用嵌入值模式映射值对象,你发现将部分违反范式设计的规则,这正是数据建模与对象建模一个重要的不一样之处。要想尽可能的发挥对象的威力,就须要弱化数据库的做用,只把他做为一个保存数据的仓库。对象建模越成功,与数据建模就会差异越大。因此当违反数据库设计原则时,不用大惊小怪,只要业务可以顺利运行,就没什么关系。
使用嵌入值进行映射的另外一个优点是可以优化查询性能,由于不须要进行联表,单表索引调优也要容易得多。
嵌入值映射基本没什么反作用,它是单个值对象的标准映射方式。可是,嵌入值映射只能映射单个值对象,若是值对象是一个集合会怎样?
继续咱们的员工管理模块,客户要求可以管理员工的教育经历、职务变更等一系列和该员工相关的附属信息,并且这些附属信息都是多行记录,好比教育经历,他从小学一直到博士的全部教育经历,须要屡次录入。从数据库的角度,就是主从表设计,员工是主表,其它都是从表。从对象的角度考虑,外层的员工是聚合根,附属的全部信息都是聚合内部的子对象,要么建模成实体,要么建模成值对象,它们从概念上构成一个总体,即聚合。
如今先来看传统的主从表建模方式,每一个附属信息都须要建立一个表,并映射成一个实体。若是附属信息有10种,那么一共须要建立11个表,能够看到,表数据大量增长,从而致使系统变得复杂。另外,考虑员工管理在界面上的操做,能够在界面上放一个选项卡来显示员工的每项附属信息,如今若是要添加员工的教育经历,一种简单的方法是在添加完一条教育经历之后当即保存并刷新。但有时为了易用性等考虑,容许客户在界面上随意操做,并在最后一步点击保存按钮一次性提交。把一个包含多个实体集合的聚合提交到服务端进行持久化,这可能很是复杂,须要从数据库中将聚合取出,而后经过标识判断出每一个子实体,哪些是新增的,哪些是修改的,哪些是已经删除的。
若是把实体换成值对象,状况就大不相同了,将大幅简化系统设计。前面介绍了单个值对象经过嵌入值模式映射,那么如今是值对象集合,如何映射呢?因为你不可能把值对象集合的每一个元素映射到外层的实体表中,可是建立多个表又增长复杂性,因此一个变态的方法是使用序列化大对象模式。把一个值对象的集合直接序列化到表中的一个字段中,这甚至违反了数据建模第一范式。能够看到,这种保存数据的方式已经颠覆了你平时的习惯。
说到这里,不少人可能准备质疑这个示例的建模方案了,这些子对象能不能被建模成值对象,甚至应不该该放到员工聚合中都要看具体状况,须要考虑多方面因素,诸如业务需求,查询需求,并发和性能需求等,如今假设,员工的附属信息使用值对象建模没什么问题,咱们来看看对系统的简化有多大改观。
首先,11个表被简化成了1个表,在表中增长了10个列而已。这个简化简直惊人。
另外再来看看界面上的操做,若是须要一次性提交整个聚合,因为值对象没有标识,并且是总体替换的,因此你不须要从数据库中把聚合拿出来做比较,只须要从新一个序列化,就万事大吉。
从上面能够看出,值对象能够帮你大幅简化持久化方面的工做,这都打动不了你,我确实也无话可说。
不变性是值对象的一个基本特征,为什么要如此严格的规定?有几个缘由:
想一想看,咱们如今讨论的值对象,它的不变性与.Net提供的值类型struct如此类似,那么是否是应该使用struct建模值对象呢?不行,缘由以下:
当使用嵌入值模式进行映射时,在聚合表中,能够根据层次关系命名列名。好比员工聚合中的地址值对象的城市属性,能够命名为:Employee_Address_City,或者Address_City,这样能够更清晰的表达子对象的映射关系。
使用值对象的第一个挑战来自关系数据库。
从上面的例子能够看到,值对象能够极度简化系统设计是由于采用了序列化大对象模式。可是这种设计方式存在不少弊端,最重要的是致使搜索值对象属性值变得异常困难。好比,客户提出,须要根据员工教育经历的学校名称进行搜索,以查找哪些员工在某个学校曾经读过。
采用序列化大对象模式,一种方式是序列化成二进制流,而后保存到Sql Server的varbinary(MAX)字段中。若是采用这种方式存储,当咱们要搜索教育经历的学校名称时,只能把全部员工读取到内存进行过滤。除此以外,当你直接查看数据库时,将彻底不知所云,相信你不会牛B到能读懂二进制流的境界。还有一个问题是,当值对象的结构发生变化,好比你增长了几个属性,可能在反序列化时失败。因此这种方式不被推荐。
另外一种方式是序列化成文本流,保存到Sql Server的nvarchar(MAX)字段中。你能够选择XML格式,或者JSON格式。通常来说JSON要好得多,不只占更少空间,并且更加简单清晰。当咱们要搜索教育经历的学校名称时,能够在nvarchar(MAX)字段中经过Like进行搜索,这样虽然不是过高效,但比起读取所有员工实体进行过滤仍是要强些。
值对象集合的搜索解决办法以下:
使用值对象的另外一个挑战来自表现层界面。
值对象的一个关键设计是支持不变性,这意味着值对象的每一个属性都没有setter,或者setter只在对象内部容许访问,这对咱们有什么影响呢?
如今你的表现层正在使用Mvc或Wpf,它们都支持模型绑定。当你在Mvc表单界面进行输入以后,提交到控制器操做,你能够在控制器操做上使用一个实体来接收参数。想像一下,你如今须要把员工地址传递到控制器操做,但因为Address是不可变的,从而致使模型绑定失败。
为了解决这个问题,使用值对象的必备条件是建立一个配套的可变值对象,对于Address,你能够给这个可变值对象取名为AddressViewModel,或者AddressDto都行,我通常叫它AddressInfo。这个对象的全部属性都有setter,而且是public的,这样才能够在表现层使用,而后它会转换成值对象,供领域层使用。
从以上能够看出,虽说考虑领域模型时,不要考虑数据库和界面,但最终这两个大环境对设计决策是可能形成影响的。
本篇为你们简要介绍了值对象,下一篇咱们将完成值对象层超类型的开发。
.Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。
谢谢你们的持续关注,个人博客地址:http://www.cnblogs.com/xiadao521/