`
近乎sns
  • 浏览: 12474 次
  • 性别: Icon_minigender_2
  • 来自: 青岛
文章分类
社区版块
存档分类
最新评论

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

阅读更多

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

  如果说你已经在使用DDD分层架构,但你却从来没有使用过值对象,这毫不奇怪,因为多年来养成的数据建模思维已经牢牢把你禁锢,以致于你在使用面向对象方式进行开发时,还是以数据为中心。

  当我们完成了基本的需求分析以后,如果说需要进行设计,那么你能想到的就是数据库表及表关系的设计,这就是数据建模。数据建模的主要依据是数据库范式设计,根据要求严格程度的递增分为第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. 《实现领域驱动设计》一书非常经典,建议你直接买了。

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

文章来至:近乎sns开发社区

分享到:
评论

相关推荐

    DDD分层架构参考代码目录结构

    《DDD分层架构及其在微服务中的应用》 DDD(Domain-Driven Design,领域驱动设计)是一种软件开发方法,强调以业务领域为中心进行系统设计。其分层架构模型是DDD的核心设计模式,它将系统分为用户接口层、应用层、...

    DDD分层架构的三种模式.

    在这篇文章中,我们将介绍 DDD 分层架构的三种模式。 在讨论 DDD 分层架构的模式之前,我们先一起回顾一下 DDD 和分层架构的相关知识。DDD 是一种软件开发方法,它可以帮助我们设计高质量的软件模型。在正确实现的...

    DDD实战篇:分层架构的代码结构

    不同于其它的架构方法,领域驱动设计DDD(DomainDrivenDesign)提出了从业务设计到代码实现一致性的要求,不再对分析模型和实现模型进行区分。也就是说从代码的结构中我们可以直接理解业务的设计,命名得当的话,非...

    领域驱动设计(DDD):分层架构

    在应用系统开发中,采用严格的、单一的、真正的的分层架构是可以的,但实际上我们已经采用了多种架构模式设计系统。当多种不同范式的架构混合在一起,你会不会出现“指鹿为马”的现象呢? 在研究分层架构时,常通过...

    spring-ddd:一个使用Spring Boot和DDD提出的分层架构的项目示例

    **Spring Boot与领域驱动设计(DDD):构建分层架构** Spring Boot是Java开发中的一个热门框架,它简化了创建独立、生产级别的基于Spring的应用程序的过程。通过内嵌的Tomcat服务器、自动配置和起步依赖,Spring ...

    基于DDD和微服务中台架构与实现

    - "实体"和"值对象"是DDD的两个基本元素,实体具有唯一标识,值对象则关注数据的完整性。 - "聚合"是DDD中的核心设计单元,包含一组相关对象,确保业务规则的一致性。 - "事件"和"事务一致性"用于处理业务流程中...

    .NET分层架构设计模式

    .NET 分层架构是一种常见的软件开发模式,它将复杂的系统分解为多个独立的层次,每个层次都有特定的职责,以此提高代码的可维护性、可扩展性和可重用性。以下是对.NET分层架构的详细解释: 1. **基本原则**: - **...

    一个超级简单的DDD领域架构,大致流程步骤有注释

    2. **聚合根**:聚合是DDD中的一个基本单元,它由一个主实体(聚合根)和一些相关联的实体或值对象组成。聚合根是聚合的入口点,负责维护内部的业务规则一致性。例如,可能有一个`Order`作为聚合根,管理订单相关的...

    DDD 领域驱动设计 架构(分层六边形RESTful).docx

    本文将详细介绍 DDD 领域驱动设计架构,并着重于分层架构、六边形架构和 RESTful 架构的概念和应用。 分层架构 分层架构是一种历史悠久的架构,通过将系统按不同职责组织成有序层次。这种架构的优点是可以将领域...

    DDD领域驱动设计和中台实践资料合集25篇.zip

    架构分层模型适配 金融支付系统的改造之路 可视化的遗留系统微服务改造 领域建模的易与难 领域驱动架构透析与架构解耦-张逸 领域驱动建模(Evans DDD) 领域驱动设计(DDD)架构的实践 如何让DDD落地 淘宝应用架构升级 -...

    net 分层架构实战

    ### .NET分层架构实战知识点解析 #### 一、综述 在.NET分层架构实战中,我们将通过一系列文章深入探讨如何构建一个基于分层架构的简单留言本系统——NGuestBook。本系列旨在帮助读者理解和掌握分层架构的基本概念...

    DDD促进传统架构微服务转型(42页).pdf

    《DDD促进传统架构微服务转型》是一份深入探讨领域驱动设计(Domain-Driven Design,简称DDD)在微服务转型中的应用的研究报告。这份42页的文档详细阐述了如何利用DDD的方法论帮助传统架构实现向微服务架构的转变,...

    DDD对架构师培养的正面影响-SACC2021年中国系统架构师大会.pdf

    DDD对架构师培养的正面影响-SACC2021年中国系统架构师大会

    DDD领域驱动设计学习框架简介PPT

    在DDD中,领域分层架构是一个关键的概念。通常包括表示层、应用层、领域层和基础设施层。表示层负责用户交互,应用层协调任务并调用领域层的服务,领域层包含核心业务规则和逻辑,而基础设施层则提供技术性的支持,...

    实现领域驱动设计-DDD架构设计思路-弗农著【高清版】

    《实现领域驱动设计》共分为14 章,在DDD 战略部分,《实现领域驱动设计》向我们讲解了领域、限界上下文、上下文映射图和架构等内容,战术部分包括实体、值对象、领域服务、领域事件、聚合和资源库等内容。...

    浅析.NET逻辑分层架构

    在进一步深入架构设计时,讨论了DDD(领域驱动设计)分层架构的不同实现方式和它们的优缺点。DDD分层架构的核心是将业务逻辑层进一步细分为应用层(服务层)和领域层(领域逻辑层),并将数据访问和其他具体技术实现...

    DDD开源架构免费领取

    在软件架构中,DDD强调业务专家与开发团队的紧密协作,以确保软件系统能准确反映业务领域的核心概念。 DDD的核心概念包括: 1. **领域**:业务的核心部分,由一系列相关的业务规则和过程组成。 2. **领域模型**:...

    smart-lottery抽奖系统基于COLA架构采用DDD领域驱动中四层架构

    DDD的核心概念包括聚合根、实体、值对象、领域事件、工厂、仓储等,这些元素帮助开发者构建出高度契合业务需求的模型。 在这个“smart-lottery”抽奖系统中,四层架构可能是这样的: 1. **表现层(Presentation ...

    在一个实际复杂业务中落地DDD方法与相关架构(31页).pdf

    2. 领域模型设计:根据业务模型分析结果,设计领域模型,包括实体、值对象、领域事件等。 3. 应用架构设计:根据领域模型,设计应用架构,包括应用层、业务层、基础设施层等。 在领域模型设计中,需要遵循以下原则...

Global site tag (gtag.js) - Google Analytics