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

  前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持。本篇将介绍另外一个重要的构造块——值对象,它是聚合中的主要成分。数据库

  若是说你已经在使用DDD分层架构,但你却历来没有使用过值对象,这绝不奇怪,由于多年来养成的数据建模思惟已经紧紧把你禁锢,以至于你在使用面向对象方式进行开发时,仍是以数据为中心。c#

  当咱们完成了基本的需求分析之后,若是说须要进行设计,那么你能想到的就是数据库表及表关系的设计,这就是数据建模。数据建模的主要依据是数据库范式设计,根据要求严格程度的递增分为第N范式,基本的要求是把每一个标量属性值用单独的一列来存储,每一个非键属性必须彻底依赖于键属性。数据库范式设计的目标是消除存储在多个位置上的冗余数据,以避免致使更新异常。为了达到这个目的,须要进行不断的表拆分,直到每一个表都只表示一个单一的概念。这能够认为是SRP(单一职责原则)在表上的应用,从而使表中的数据产生更高的内聚性。这从数据库的角度看多是不错的,但对于面向对象开发却不见得是个好事。安全

  每个表称为一个数据库实体。当你完成了表设计之后,很天然的把数据库实体与DDD实体等同起来,这产生了一个直观的映射,因此每一个表在你的系统中都是一个实体。受这个根深蒂固的开发模式影响,你与值对象无缘相见。架构

  值对象不只在概念上提供强大的帮助,并且在技术上,特别是持久化方面可以大幅简化系统设计,后面我将逐步介绍聚合与值对象是如何帮助你下降系统复杂性而脱困的。并发

什么是值对象                                                  

  经过对象属性值来识别的对象,它将多个相关属性组合为一个概念总体。框架

  在值对象的概念中,隐含了以下信息:异步

  1. 值对象能够对某些简单业务概念建模。
  2. 值对象没有标识。值对象比实体简单得多,不须要跟踪变化,因此它没有标识。
  3. 值对象是不可变的。这是值对象的核心特征,后面将详述。
  4. 值对象的相等性比较是经过各个属性值的比较来完成的。
  5. 因为值对象表明一个概念总体,因此只能进行总体替换,而不是修改值对象的某个属性。

值对象的价值                                                  

  看了上面的概念描述,可能并不能打动你。你会说“实体不就比值对象多一个标识,能复杂到哪去”。因为你使用实体一样能够对业务概念建模,因此是否使用值对象,对你来讲根本不重要。数据库设计

  下面来看看使用值对象的其它好处。性能

  值对象的一个做用是能够帮助优化性能。当一个值对象须要在多个地方使用时,能够共享同一个值对象。为了共享同一个值对象,你可使用工厂来建立单例模式的值对象实例,因为值对象是不可变的,因此能够安全的使用。测试

  固然,你可能对使用值对象来提高性能也不感兴趣,你须要更实在的好处,不然就免谈。下面将介绍值对象的重型武器,它对你将产生空前的影响,甚至颠覆你平时的建模习惯和开发模式。

  前面已经说过,你为了知足数据库规范化设计,建立大量的表,各个表之间关系错综复杂,并且你也意识到正是表的膨胀致使了系统复杂性的上升。若是可以减小表的数量,那么表之间的关系也会变得简单和清晰,有什么办法能够减小表的数量吗?答案就是值对象与逆范式设计。

  首先来看一个简单状况。如今要为人力资源系统创建员工档案,咱们使用一个名为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个列而已。这个简化简直惊人。

  另外再来看看界面上的操做,若是须要一次性提交整个聚合,因为值对象没有标识,并且是总体替换的,因此你不须要从数据库中把聚合拿出来做比较,只须要从新一个序列化,就万事大吉。

  从上面能够看出,值对象能够帮你大幅简化持久化方面的工做,这都打动不了你,我确实也无话可说。

值对象的设计要点                                                  

  值对象必须不可变。

  不变性是值对象的一个基本特征,为什么要如此严格的规定?有几个缘由:

  1. 值对象表明的就是一个值,这个值是一个总体,若是须要修改,必须整个替换,不能部分修改。这是从概念上说明值对象的不变性。
  2. 为了安全的使用值对象,防止别名Bug。前面说过,值对象的一个做用是优化性能,减小内存占用,这是经过共享同一个值对象来实现的。若是值对象容许修改,当一个值对象被多个其它对象共享时,若是其中一个对象改变了值对象的某个属性值,这个改变在其它对象上也会立刻生效,可能致使严重的问题,这被称为别名Bug。另外,将值对象进行引用传递时,值对象在其它代码中可能发生任何操做。这是从技术上保证值对象只有不可变,才能安全的使用,否则随时可能担忧吊胆,当发生Bug时也很难跟踪。
  3. 当把值对象做为Dictionary这样的哈希集合的键时,哈希集合会使用值对象的GetHashCode计算出一个地址,并将值保存在这里,以后,若是须要查找一个值,经过值对象的GetHashCode从新计算出该地址,而后把值提取出来。若是值对象是可变的,当把数据保存到哈希集合以后,修改了值对象,那么经过值对象从新计算出来的hashcode可能不一样,从而丢失了这个值。

  使用object建模值对象,而不是struct。

  想一想看,咱们如今讨论的值对象,它的不变性与.Net提供的值类型struct如此类似,那么是否是应该使用struct建模值对象呢?不行,缘由以下:

  1. struct用来实现基元类型,好比int,这些类型都很是小,专家建议不要超过16字节大小。咱们如今的值对象虽然比实体可能简单些,但仍是可能很庞大。一个比较大的对象,从性能上考虑,放入堆中进行垃圾回收更合适,实际上string就是一个值对象。
  2. 若是使用像Entity Framwork这样的ORM框架,它可能不支持struct的映射。

  嵌入值模式映射列名能够遵循必定命名规则。

  当使用嵌入值模式进行映射时,在聚合表中,能够根据层次关系命名列名。好比员工聚合中的地址值对象的城市属性,能够命名为:Employee_Address_City,或者Address_City,这样能够更清晰的表达子对象的映射关系。

使用值对象的挑战                                                  

  使用值对象的第一个挑战来自关系数据库。

  从上面的例子能够看到,值对象能够极度简化系统设计是由于采用了序列化大对象模式。可是这种设计方式存在不少弊端,最重要的是致使搜索值对象属性值变得异常困难。好比,客户提出,须要根据员工教育经历的学校名称进行搜索,以查找哪些员工在某个学校曾经读过。

  采用序列化大对象模式,一种方式是序列化成二进制流,而后保存到Sql Server的varbinary(MAX)字段中。若是采用这种方式存储,当咱们要搜索教育经历的学校名称时,只能把全部员工读取到内存进行过滤。除此以外,当你直接查看数据库时,将彻底不知所云,相信你不会牛B到能读懂二进制流的境界。还有一个问题是,当值对象的结构发生变化,好比你增长了几个属性,可能在反序列化时失败。因此这种方式不被推荐。

  另外一种方式是序列化成文本流,保存到Sql Server的nvarchar(MAX)字段中。你能够选择XML格式,或者JSON格式。通常来说JSON要好得多,不只占更少空间,并且更加简单清晰。当咱们要搜索教育经历的学校名称时,能够在nvarchar(MAX)字段中经过Like进行搜索,这样虽然不是过高效,但比起读取所有员工实体进行过滤仍是要强些。

  值对象集合的搜索解决办法以下:

  1. 根本不提供值对象属性的查询条件。这一点须要你的客户或老板通人性才行,另外也有一些技巧。若是你直接告诉老板,这个搜索功能作不了,你的老板会大发雷霆“这么简单都作不出来,我要你来干吗”。可是,若是你告诉老板不提供这几个搜索条件,能够提早两天完工,他有可能就批了。
  2. 更换成NOSQL数据库,好比MongoDB。MongoDB支持层次化存储和查询,从而从根本上解决问题。但不是每一个系统都能用上MongoDB,也不是每一个系统都适合使用MongoDB,好比你的系统须要很强的事务控制,但MongoDB只有一些有限的原子操做能力,不支持事务。
  3. 使用Like进行搜索,这在数据不太大的时候,也能凑活。
  4. 创建单独的查询数据库或表。为了提高查询效率,专门为查询建立一些表,这些表的结构按照搜索最方便的方式设计,这样将查询与操做分离开来。这样作的问题是比较麻烦,另外致使复杂度上升,但它可以兼顾操做的简便性和查询性能,因此也不失为一种解决方法。使用这种方法须要将数据保存两份,在同一事务中采用同步更新可能致使更新上的性能损失。若是采用异步方式更新,虽然性能提高,又可能致使更新延时,形成界面显示异常等问题。
  5. 转成实体。若是上面的方法,你以为都很差,可能转成实体更简单方便。
  6. 在《实现领域驱动设计》一书中,提供了另外一种设计方案,它采用实体的表设计方式,而后在值对象的层超类型中隐藏标识,这样在代码中感受它仍是一个值对象,同时又可查询。不过我我的不是太喜欢这个方案,我若是建立了单独的表,可能使用实体更方便。

  使用值对象的另外一个挑战来自表现层界面。

  值对象的一个关键设计是支持不变性,这意味着值对象的每一个属性都没有setter,或者setter只在对象内部容许访问,这对咱们有什么影响呢?

  如今你的表现层正在使用Mvc或Wpf,它们都支持模型绑定。当你在Mvc表单界面进行输入以后,提交到控制器操做,你能够在控制器操做上使用一个实体来接收参数。想像一下,你如今须要把员工地址传递到控制器操做,但因为Address是不可变的,从而致使模型绑定失败。

  为了解决这个问题,使用值对象的必备条件是建立一个配套的可变值对象,对于Address,你能够给这个可变值对象取名为AddressViewModel,或者AddressDto都行,我通常叫它AddressInfo。这个对象的全部属性都有setter,而且是public的,这样才能够在表现层使用,而后它会转换成值对象,供领域层使用。

  从以上能够看出,虽说考虑领域模型时,不要考虑数据库和界面,但最终这两个大环境对设计决策是可能形成影响的。

使用值对象的建议                                                  

  1. 聚合中尽可能使用值对象。值对象与实体在不少时候多是可互换的,因为值对象能够简化系统,因此当它的缺点能够克服就应该坚定采用。
  2. 值对象必须设计成不可变,而且值对象的任何方法都不能修改属性值。若是值对象的方法须要进行修改,能够经过该方法返回一个该值对象的新实例。若是对象是可变的,应该建模为实体,而不是值对象。
  3. 若是须要跟踪对象的生命周期,或者在聚合外部,须要进行标识引用,应该采用实体,而不是值对象。

最后,总结一下                                                  

你排斥值对象的主要缘由:

  1. 长期以来,咱们使用数据库所形成的思惟定势影响。
  2. 序列化大对象,形成查询不便。
  3. 不可变值对象在界面上没法绑定,须要额外建立配套的可变值对象,让你以为工做量变大。
  4. 代码生成器没法直接建立值对象,须要将生成出来的代码手工调整,你不想这么麻烦。

值对象为你提供的主要价值:

  1. 更简单,更清晰的表达简单业务概念。
  2. 帮助你优化系统性能。
  3. 帮助你简化系统设计,特别是持久化方面。

值对象的设计要点:

  1. 值对象必须不可变。
  2. 值对象的任何方法都不能直接修改属性值,能够经过该方法返回一个新实例。
  3. 使用object建模值对象,而不是struct。
  4. 当值对象是单个时,优先使用嵌入值模式映射。在EF中经过ComplexTypeConfiguration配置映射。
  5. 当值对象是集合,或者值对象的内部层次关系很复杂时,优先使用序列化大对象模式映射。
  6. 嵌入值模式映射列名能够遵循必定命名规则,好比Employee_Address_City。
  7. 序列化大对象时,优先使用Json格式保存。
  8. 为每一个值对象建立一个配套的可变值对象,以方便界面使用。

实体与值对象的区别:

  1. 实体拥有标识,而值对象没有。
  2. 相等性测试方式不一样。实体根据标识判等,而值对象根据内部全部属性值判等。
  3. 实体容许变化,值对象不容许变化。
  4. 持久化的映射方式不一样。实体采用单表继承、类表继承和具体表继承来映射类层次结构,而值对象使用嵌入值或序列化大对象方式映射。

参考                                                  

  1. 若是你对映射模式感兴趣,请参考《企业应用架构模式》第12章——对象关系结构模式。
  2. 若是你对别名Bug感兴趣,请参考《企业应用架构模式》第18章值对象一节。
  3. 若是你对建立配套可变值对象感兴趣,请参考《领域驱动设计 c# 2008实现》第97页MutableAddress类一节。
  4. 《实现领域驱动设计》一书很是经典,建议你直接买了。

  本篇为你们简要介绍了值对象,下一篇咱们将完成值对象层超类型的开发。

  .Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。

  谢谢你们的持续关注,个人博客地址:http://www.cnblogs.com/xiadao521/

相关文章
相关标签/搜索