欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

day6

程序员文章站 2022-07-13 22:30:05
...

158-254

实体(接前面)

开篇作者指出,当前软件开发数据库依然占据主导地位,导致我们设计的时候经常聚焦于数据的属性(对应数据库的列)与关联关系(外键等),而不是富有行为含义的领域概念。这样做的结果是将数据模型直接反映在对象模型上,出现很多 getter、setter 方法,DDD 不推荐这样做。

为什么使用实体

用于个性特征或区分不同对象,判断是不是同一个实体主要依据身份标识(identity),唯一身份标识和可变性(mutability)特征将实体对象和值对象(Value Object)区分开来。

虽然有时候实体不见得是唯一的建模工具,但是一旦采用了 CRUD 之后系统可能非常昂贵且不好维护,因为 CRUD 只能从数据出发,不能创建出好的业务模型。

唯一标识

设计初期,我们刻意关注一下体现实体身份唯一性的主要属性和行为上,还有如何对实体进行查询。只有在对实体的本质特征有用的情况下,才加入相应的属性和行为。

唯一标示并不见得有助于实体的查找和匹配,例如 Person 实体唯一标示极可能是人名,但是人名存在重复因此不能作为查询依据。值对象可以用于存放实体的唯一标示,值对象是不变的可以保证实体身份的稳定性,我们可以避免将身份标识相关的行为泄漏到模型的其他部分或者客户端中。

常用的创建实体身份标识的策略:

  1. 用户提供一个或多个初始唯一值作为程序输入,缺点在于用户自己生成,但是用户经常需要更改输入的标识
  2. 程序内部通过某种算法自动生成,缺点是集群环境或者分布式环境中,标识体积大且不具备可读性
  3. 数据库生成,缺点是性能低
  4. 另一限界上下文提供,缺点是我们需要对每个标识进行查找、匹配、赋值,同时对象同步也可能是问题

标识生成时间

实体生成标识既可以发生在对象创建的时候也可以发生在持久化对象的时候。
day6
上图 client 需要拿到 productId,但是是在流程插入数据库后才能返回该 Id,如果客户端在之前需要向外界发布领域事件,那么在 client 拿到 Id 之前该事件可能已经被订阅方接收到了,为了正确的创建领域事件,我们需要及早生成实体标识。

另外,延迟到持久化之后再返回标识还有个问题,例如在使用 HashSet 数据结构时,缺少唯一标识可能会让 Set 认为所有新建对象都是同一个对象,只保留一个。

day6
此时 client 先请求下一个 Id,然后 set 进实体中进行持久化。

标识的稳定性

我们需要通过一些简单的措施来包装标识不被修改,我们可以将 setter 向用户隐藏,或者在 setter 中添加逻辑以确保标识存在的情况下不会被更新等。

什么是委派标识?

无意义的id主键,

什么是领域标识?

username

发现实体及其本质特征

角色:

对象分裂症:委派对象根本不知道被委派对象的身份标识。
实体的角色在不同用例之间发生转变:

创建实体

构造函数初始化足够多的实体状态,有助于查询和表明实体身份。一个实体维护了一个或多个不变条件,不变条件是整个实体生命周期中都必须保持事务一致性的状态。不变条件是聚合所关注的,聚合根通常也是实体。

验证

验证用于检查模型的正确性。
day6
验证属性合法性:前置验证
day6
验证整体对象:验证逻辑放在哪个位置?

  • 放到实体对象中,验证逻辑比领域对象变化也快。
  • 放到领域对象中,使领域对象承担了过多的职责
  • 可以建立一个单独的组件来完成验证。
    验证对象组合:领域服务进行验证
    day6

值对象

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

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

什么是值对象

值对象=值+对象=将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。

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

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

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

度量或描述

值对象只能作为度量或描述领域中某件东西的一个概念,而不应该成为你领域中的一件东西。

不变性

一个值对象在创建之后便不能被改变,使用 final 修饰或者 private 等访问修饰符进行限制。

概念整体

一个值对象可以只处理单个属性也可以是一组相关联的属性,这里作者举了个例子,值对象{50,000,000 美元} 这是一个整体,单独的 50,000,000 和单独的美元都不能准确表达含义,那么在建模时这二者(amount和currency)便不能看成独立的变量:
day6
而是应该这样,这里整体值对象 MonetaryValue 不单单是一个起描述作用的描述属性,而是一个资产属性。
day6

值相等性

相等性通过比较两个对象的类型和属性来决定,如果两个对象的类型和属性都相等,那么两个对象即相等,Java 中通过重写 equals 方法来达到目的

无副作用行为

可以设计成一个无副作用的函数,只用于输出而不改变对象的状态

值对象的价值

**值对象的一个作用是可以帮助优化性能。**当一个值对象需要在多个地方使用时,可以共享同一个值对象。为了共享同一个值对象,你可以使用工厂来创建单例模式的值对象实例,由于值对象是不可变的,所以可以安全的使用。
  
  前面已经说过,你为了满足数据库规范化设计,创建大量的表,各个表之间关系错综复杂,而且你也意识到正是表的膨胀导致了系统复杂性的上升。如果能够减少表的数量,那么表之间的关系也会变得简单和清晰,有什么办法可以减少表的数量吗?

值对象与逆范式设计。

首先来看一个简单情况。现在要为人力资源系统建立员工档案,我们使用一个名为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个列而已。这个简化简直惊人。  
  另外再来看看界面上的操作,如果需要一次性提交整个聚合,由于值对象没有标识,而且是整体替换的,所以你不需要从数据库中把聚合拿出来作比较,只需要重新一个序列化,就万事大吉。

值对象的问题

说到问题,你可能想到的第一个问题就是持久化的问题。是的,值对象没有标识列如何存储数据库呢?当下比较流行使用ORM持久化机制,使用ORM将每个类映射到一张数据库表,再将每个属性映射到数据库表中的列会增加程序的复杂性。那如何使用ORM持久化来避免这一问题呢?

  1. 单个值对象上面我们提到值对象不会孤立存在,所以我们可以将值对象中的属性作为所属实体/聚合根的数据列来存储(比如,我们可以将收货地址的属性映射到客户实体中)。这样做就会导致数据表列数增多,但是能够优化查询性能,因为不需要联表查询。
  2. 多个值对像序列化到单个列当每个客户仅允许维护一个收货地址时,我们用上面的方式没有问题。但很显然一个客户可以有多个收货地址。这个时候我们该怎么持久化值对象集合呢?不可能把值对象集合的每个元素映射到外层的实体表中,但是创建多个表又增加复杂性,所以一个变态的方法是使用序列化大对象模式。把一个集合序列化后塞到外层实体表的某一列中,是有点匪夷所思。而且数据库的列宽是有限制的,且不方便查询。但似乎也带来一个好处,大大简化了系统的设计(不用设计多列分别存储了)。
  3. 使用数据库实体保存多个值对像使用层超类型来赋予值对象一个委派标识,以数据库实体的形式保存值对象。

值对象的作用

通过上面的分析介绍,我们可以体会到值对象带来的以下好处:

  1. 符合通用语言,更简单明了的表达简单业务概念。
  2. 提升系统性能。
  3. 简化设计,减少不必要的数据库表设计

建模值对象

值对象作为领域建模工具之一,有其存在的意义。领域中,并不是每一个事物都必须有一个唯一身份标识,对于某些对象,我们更关心它是什么而无需关心它是哪个。所以建模值对象,我们关键要结合通用语言的表述看其是否有值的含义和特征。

最小化集成

在 DDD 项目中通常存在多个限界上下文,意味着我们需要找到合适的方法对这些上下文进行集成,当模型概念从上游上下文流入下游上下文中时,尽量使用值对象来表示这些概念,这样的好处是可以达到最小化集成,既可以最小化下游模型中的属性数目,又可以使用不变的值对象减少职责假设。

用值对象表示标准模型

标准模型是用于表示事物类型的描述性对象。系统中既有表示事物的实体和描述实体的值对象,同时还存在标准类型来区分不同的类型。例如你定义了 PhoneNumber 这个值对象,同时还需要为每个 PhoneNumber 制定一个类型(家庭号码?工作号码?)。在 Java 中这种一般是通过枚举类型实现:

day6

持久化值对象

ORM 映射持久化机制是流行的,当下 NoSQL 也越来越受欢迎,因其高性能、可伸缩、容错性和高可用等特点。这部分不多说了,Spring 中应该很常见,作者主要强调了从领域模型看持久化而不要从数据模型出发。

实体对象(entity object)与值对象(value object)的区别:

实体一定要有一个唯一标识符(ID),如类实例,以确保系统能够明确的区分每一个实体,并在需要的时候准确的找到它。
值对象没有ID,这是因为系统从来不会直接去检索值对象。值对象总是从属于某个实体的。
实体有自己独立的生命周期,而值对象没有。它总是依附于某个实体。如果实体不存在了,它也将一同消亡。
不会出现两个以上的实体引用一个值对象的情况。这也是对2一个保证。如果两个实体有同样的值,那也只可能是有两个值一样的值对象,而不是引用同一个值对象

领域服务(书上讲的一知半解,这里我看的其他资料)

链接 https://blog.csdn.net/FS1360472174/article/details/87824637
领域对象与领域服务的

(书上)
领域中的服务表示无状态操作,实现某个领域的任务。

day6

什么是领域服务

作者罗列以下几点可以考虑使用领域服务:执行一个显著的业务操作过程对领域对象进行转换以多个领域对象作为输入进行计算,结果产生一个值对象(这点需要该操作具有显著的业务操作过程特点)

建模领域服务

根据创建领域服务的目的,需要决定你所创建的领域服务是否需要一个独立接口,例如:
day6

上述服务用来认证一个 User该接口和那些与身份有关的聚合定义在相同的模块中,因为 AuthenticationService 也是一个与身份有关的概念,该接口只有一个 authenticate 方法。实现类可以选择性将其放在不同的地方,如果使用依赖导致原则或六边形架构,你可能会将实现类放在领域模型之外。

独立接口有必要么

由于这里 AuthenticationService 并没有技术上的实现,有必要为其创建独立接口并将其与实现类分离在不同的层和模块中么?其实是没有必要的,我们主要创建一个实现类即可,名字与领域服务相同。

领域服务实现类命名

Java 中通常是加上 Impl 后缀,事实上采用这种命名通常意味着你根本不需要一个独立接口。如果我们采用了依赖注入或者工厂,即便接口和实现类是合并在一起的,我们依然能达到这样的目的。依赖倒置容器(例如 Spring)将完成服务实例的注入工作,由于客户端并不负责服务的实例化,它并不知道接口和实现类是分开的还是合并在一起的。

领域服务与应用服务

单从字面理解,不管是领域服务还是应用服务,都是服务。而什么是服务?从SOA到微服务,它们所描述的服务都是一个宽泛的概念,我们可以理解为服务是行为的抽象。从前缀来看,根据DDD的经典分层架构,它们又隶属于不同的层,应用服务属于应用层,领域服务属于领域层。

day6

  • 应用层(Application):负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序任务。它不包含业务逻辑。
  • 领域层(Domain):负责表达业务概念,业务状态信息以及业务规则,是业务软件的核心。

领域服务是用来协调领域对象完成某个操作,用来处理业务逻辑的,它本身是一个行为,所以是无状态的。状态由领域对象(具有状态和行为)保存。

上面也说了,领域对象是具有状态和行为的。那就是说我们也可以在实体或值对象来处理业务逻辑。那我们该如何取舍呢?
一般来说,在下面的几种情况下,我们可以使用领域服务:

  1. 执行一个显著的业务操作过程
  2. 对领域对象进行转换
  3. 以多个领域对象为输入,返回一个值对象。

我们拿经典的转账问题来分析一下:
而针对转账这一操作,它的业务用例应该是这样的:

  1. 检查账号余额是否足够
  2. 检查目标账户账号是否合法
  3. 转账短信通知转账双方

其中1,2步是转账的合法性校验属于转账业务的一部分,所以,1,2,3均应该放到领域层通过领域服务来实现。短信通知,它并不是是转账的核心业务,因为这根据具体情况而定,比如只有客户订阅了账号变动通知我才发短信。所以将第4步归类到应用服务中去实现,就确保了领域服务的纯粹性。

而至于持久化的问题,我们可以这样想,领域逻辑应该只关心业务逻辑,才能保证领域逻辑的可重用性。将持久化放到应用层,我们就会有更多的选择性。

相关标签: 笔记