7、实体和值对象:领域模型的基础单元

        DDD战术设计中有两个重要的概念:实体(Entity)和值对象(Value Object)。二者是领域模型中非常重要的基础领域对象(Domain Object,DO)。

从DDD战略设计到战术设计会经历从业务建模到技术落地的多个不同阶段,阶段不同,这些领域对象的形态表现也会不同。在用户旅程分析或场景分析构建领域模型时,实体和值对象是偏业务领域的,主要体现为业务属性和业务行为。而当它们从领域模型映射到代码模型时,这些领域对象会变成代码对象,这时候的我们会更关注这些领域对象的依赖关系,关注如何一起按照聚合的业务规则实现业务逻辑。当这些领域对象持久化存储到数据库时,它们的名称和状态可能又会发生变化,此时我们需要将这些领域对象转换为持久化对象(Persistent Object,PO),完成数据的持久化。

所以,理解和区分实体和值对象在不同阶段的形态很重要,形态发生了变化,我们就需要对它进行转换。这些内容与微服务设计和代码实现有着非常密切的关系。

那么,实体和值对象在领域模型中起到什么样的作用?在战术设计时又该如何将它们映射到代码模型和数据模型中去呢?这是我们这一章要重点讲解的内容。带着这些问题,我们看看能不能从文章中找到答案。

1、 实体

我们先来看看实体是什么?

在DDD的领域模型中有这样一类对象,它们拥有唯一标识符,并且它们的标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是属性,而是其延续性和标识,这种对象的延续性和标识会跨越甚至超出软件的生命周期。我们把领域模型中这样的领域对象称为实体。

没理解?没关系!请继续阅读。

  1. 实体的业务形态

在DDD不同的设计阶段中,实体的形态是不同的。

在战略设计时,实体是领域模型的一个重要对象,它是业务形态的业务对象,集多个业务属性、业务操作或行为于一体。在进行用户旅程或业务场景分析时,我们可以根据命令、业务操作或者领域事件,找出产生这些业务行为的实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。

你可以这么理解,实体和值对象是组成领域模型的基础单元。

  1. 实体的代码形态

在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务行为和业务逻辑。

DDD更强调面向对象的设计方法。这些实体类通常采用充血模型,与实体相关的所有业务逻辑都在实体类方法中实现,跨多个实体的领域逻辑则在领域服务中实现。

注意:

充血模型与贫血模型的关键差异:

在充血模型中,业务逻辑都在领域实体对象中实现,实体本身不仅包含了属性,还包含了它的业务行为。DDD领域模型中实体是一个具有业务行为和逻辑的对象。

而在贫血模型中领域对象大多只有setter和getter方法,业务逻辑统一放在业务逻辑层实现,而不是在领域对象中实现。

  1. 实体的运行形态

        实体以领域对象(DO)的形式存在,每个实体对象都有唯一的ID。

我们可以对一个实体对象进行多次修改,修改后的实体数据和原来的数据可能会大不相同。但是,由于拥有相同的ID,它们依然是同一个实体。

比如商品是商品限界上下文的一个实体,通过唯一的商品ID来标识。不管这个商品的数据如何变化,商品的ID一直保持不变,所以它始终是同一个商品。

  1. 实体的数据库形态

        与传统数据模型设计优先不同,DDD是先构建领域模型,通过场景分析找出实体对象和行为,再将实体对象映射到数据持久化对象。

在领域模型映射到数据模型时,一个实体可能对应0个、1个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。

在某些场景中,有些实体只是暂驻内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。

而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户user与角色role两个持久化对象可生成权限实体,一个DO实体会对应两个持久化对象,这是一个一对多的场景。

再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息customer和账户信息account两类数据保存到同一张数据库表中。客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。

2、 值对象

        相对实体而言,值对象会更加抽象一些,在讲解概念时,我们会结合例子来讲。

我们先看一下《实现领域驱动设计》书中对值对象的定义:

        值对象是通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体,用于描述领域的某个特定方面,并且是一个没有标识符的对象。

也就是说,值对象描述了领域中的某一个东西,这个东西是不可变的,它将不同的关联属性组合成了一个概念整体。当度量和描述改变时,我们可以用另外一个值对象予以替换。它可以和其他值对象进行相等性比较,不过不是基于ID,而是基于值对象的属性。因为不可修改的特性,它不会对协作对象带来副作用。

上面这两段对于值对象定义的阐述,可能还会有些晦涩,下面用更通俗的语言把定义讲清楚。

        简单来说,值对象本质是一个属性集合,那这个集合里面有什么呢?它们是若干个基于描述目的、具有整体概念和不可修改的属性,在应用运行时,我们主要关注这些属性集的“值”。这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免出现零碎的属性。值对象通过抽象或标准化设计,可以采用数据冗余的方式在不同的业务领域实现数据流转。

这里举个简单的例子:

        人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样在人员实体中,显示地址的多个属性就会显得很零碎了,对不对?

现在,我们可以将“省、市、县和街道”等属性拿出来,构成一个地址的属性集合,这个属性集合的名称就是地址值对象。

值对象的业务形态

        值对象是领域模型中的一个基础对象,它跟实体一样都来源于事件风暴所构建的领域模型,都包含若干个属性,并与实体一起构成聚合。

下面我们不妨对照实体来看值对象的业务形态,这样就更好理解了。

        实体和值对象都是若干属性的集合。实体一般是看得到、摸得着的实实在在的业务对象,具有业务属性、业务行为和业务逻辑。而值对象虽然也是若干个属性的集合,但它只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上你仍然可以认为它是实体属性的一部分,用于描述实体的特征。

        在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的、提供查询服务的数据类微服务,比如数据字典。

值对象的代码形态

        值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性。如果值对象是属性集合,则将它设计为值对象类,这个类将具有整体概念的多个属性归集到属性集合,这样的值对象类在代码中有更好的独立性和复用性。

值对象的运行形态

        值对象的运行形态与业务形态和代码形态基本一致,它们在运行时是不可变的对象。由于不可变性,值对象是线程安全的,可以在多个线程中共享,从而提高系统性能。

鉴于值对象比实体更轻量级、高性能且线程安全,所以一般建议将领域对象优先设计为值对象,而非实体。你可以对照着以上这些优劣势,结合你的业务场景,好好想一想。如果在你的业务场景

中,值对象的这些劣势都可以避免掉,那就请放心大胆地使用值对象吧。

3、 实体和值对象的关系

        实体和值对象都是微服务底层的最基础的领域对象,一起实现领域模型最基本的核心领域逻辑。值对象和实体在某些场景下可以互换。

        其实,很多DDD专家在某些场景下,也很难判断到底应该将领域对象设计成实体还是值对象。可以说,值对象在某些场景下可以带来很好的价值,但并不是所有场景都适合值对象。你需要根据团队的设计和开发习惯,以及上面的优势和局限分析,选择最适合的实现方式。

        另外,很多值对象的数据可能来源于其他聚合,它们以数据冗余的方式完成不同领域中数据的流转和共享。在值对象的数据源头聚合,以实体或聚合根的形式存在,完成实体和数据的集中维护和生命周期管理。而在自己的聚合中它则以值对象的形式存在,被聚合内的某一个实体引用。例如:在订单聚合中,订单实体有收货地址这个值对象。在生成订单实体时,会从个人中心的客户聚合中,获取地址实体数据组合成订单聚合的地址值对象。订单实体可以整体引用和修改地址值对象的数据,但不允许单独修改地址值对象的某一个属性数据,如street。所有地址数据的新增和修改等维护操作,都只能在客户聚合中完成,这样就可以实现业务职责的高内聚,也就是说,如果你要修改某个业务行为或数据,只需要修改一处就可以了。

4、 本章小结

        本章介绍了DDD中两个非常重要的基础概念——实体和值对象。它们是领域模型的基础单元,是构建领域模型的重要组成部分。理解和区分实体和值对象的不同形态和作用,对于领域建模和微服务设计至关重要。通过本章的学习,希望你能够更好地理解实体和值对象的概念,并在实际项目中合理应用它们。

==============

举例说明:

广告业务中的实体和值对象示例

实体(Entity)
  1. 广告活动(AdCampaign)

    • 唯一标识符:每个广告活动都有一个唯一的ID。
    • 独立生命周期:广告活动可以创建、修改、暂停和结束。
    • 业务意义:广告活动的状态和属性变化对业务有重要影响。
    public class AdCampaign {
        private final String campaignId;
        private String name;
        private double budget;
        private LocalDateTime startDate;
        private LocalDateTime endDate;
    
        public AdCampaign(String campaignId, String name, double budget, LocalDateTime startDate, LocalDateTime endDate) {
            this.campaignId = campaignId;
            this.name = name;
            this.budget = budget;
            this.startDate = startDate;
            this.endDate = endDate;
        }
    
        // Getter and Setter methods
    }
    
  2. 广告客户(Advertiser)

    • 唯一标识符:每个广告客户都有一个唯一的ID。
    • 独立生命周期:广告客户的资料可以独立管理,包括创建、更新和删除。
    • 业务意义:广告客户的信息对业务的管理和广告活动的执行有重要影响。
    public class Advertiser {
        private final String advertiserId;
        private String name;
        private ContactInfo contactInfo;
    
        public Advertiser(String advertiserId, String name, ContactInfo contactInfo) {
            this.advertiserId = advertiserId;
            this.name = name;
            this.contactInfo = contactInfo;
        }
    
        // Getter and Setter methods
    }
    
  3. 广告订单(AdOrder)

    • 唯一标识符:每个广告订单都有一个唯一的ID。
    • 独立生命周期:广告订单有独立的生命周期,可以创建、更新和删除。
    • 业务意义:广告订单的状态和属性变化对业务有重要影响。
    public class AdOrder {
        private final String orderId;
        private String campaignId;
        private String advertiserId;
        private List<AdSlot> adSlots;
        private LocalDateTime orderDate;
    
        public AdOrder(String orderId, String campaignId, String advertiserId, LocalDateTime orderDate) {
            this.orderId = orderId;
            this.campaignId = campaignId;
            this.advertiserId = advertiserId;
            this.orderDate = orderDate;
            this.adSlots = new ArrayList<>();
        }
    
        // Getter and Setter methods
    }
    
  4. 设备(Device)

    • 唯一标识符:每个设备都有一个唯一的ID。
    • 独立生命周期:设备的状态和属性可以独立管理。
    • 业务意义:设备在广告投放中的状态和配置对业务有重要影响。
    public class Device {
        private final String deviceId;
        private String location;
        private List<AdSlot> adSlots;
    
        public Device(String deviceId, String location) {
            this.deviceId = deviceId;
            this.location = location;
            this.adSlots = new ArrayList<>();
        }
    
        // Getter and Setter methods
    }
    
值对象(Value Object)
  1. 联系方式(ContactInfo)

    • 无唯一标识符:联系方式只是信息的集合,不需要唯一标识符。
    • 不可变性:联系方式一旦设定通常不会改变,如果需要修改,可以创建一个新的联系方式值对象。
    • 描述特征:用于描述广告客户的联系方式。
    public class ContactInfo {
        private final String email;
        private final String phone;
    
        public ContactInfo(String email, String phone) {
            this.email = email;
            this.phone = phone;
        }
    
        public String getEmail() {
            return email;
        }
    
        public String getPhone() {
            return phone;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ContactInfo that = (ContactInfo) o;
            return email.equals(that.email) && phone.equals(that.phone);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(email, phone);
        }
    }
    
  2. 预算金额(BudgetAmount)

    • 无唯一标识符:预算金额只是一个数值,不需要唯一标识符。
    • 不可变性:预算金额一旦设定就不会改变,如果需要调整预算,可以创建一个新的预算金额值对象。
    • 描述特征:用于描述广告活动的预算。
    public class BudgetAmount {
        private final double amount;
    
        public BudgetAmount(double amount) {
            this.amount = amount;
        }
    
        public double getAmount() {
            return amount;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            BudgetAmount that = (BudgetAmount) o;
            return Double.compare(that.amount, amount) == 0;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(amount);
        }
    }
    
  3. 时间段(TimePeriod)

    • 无唯一标识符:时间段只是时间点的组合,不需要唯一标识符。
    • 不可变性:时间段一旦设定就不会改变,如果需要修改,可以创建一个新的时间段值对象。
    • 描述特征:用于描述广告投放的时间段。
    public class TimePeriod {
        private final LocalDateTime startTime;
        private final LocalDateTime endTime;
    
        public TimePeriod(LocalDateTime startTime, LocalDateTime endTime) {
            this.startTime = startTime;
            this.endTime = endTime;
        }
    
        public LocalDateTime getStartTime() {
            return startTime;
        }
    
        public LocalDateTime getEndTime() {
            return endTime;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            TimePeriod that = (TimePeriod) o;
            return startTime.equals(that.startTime) && endTime.equals(that.endTime);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(startTime, endTime);
        }
    }
    
  4. 广告素材(AdContent)

    • 无唯一标识符:广告素材主要关注其内容,不需要唯一标识符。
    • 不可变性:广告素材一旦设定就不会改变,如果需要修改,可以创建一个新的广告素材值对象。
    • 描述特征:用于描述广告的具体内容。
    public class AdContent {
        private final String contentUrl;
    
        public AdContent(String contentUrl) {
            this.contentUrl = contentUrl;
        }
    
        public String getContentUrl() {
            return contentUrl;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            AdContent adContent = (AdContent) o;
            return contentUrl.equals(adContent.contentUrl);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(contentUrl);
        }
    }
    
结论

在广告业务中,合理地区分实体和值对象可以帮助我们更好地建模业务需求和实现系统。实体用于表示具有唯一标识符和独立生命周期的重要业务对象,而值对象用于描述这些对象的属性和特征,通过不可变性确保其一致性和可靠性。

理解和应用实体和值对象的概念,可以提高系统的可维护性和扩展性,使我们的领域模型更加清晰和健壮。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/771717.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

@PostConstruct注解

1.简介 PostConstruct是java5的时候引入的注解&#xff0c;主要用于标记一个方法&#xff0c;表示该方法应在依赖注入完成后自动调用。通常在使用Java EE或者Spring框架时使用这个注解&#xff0c;以便在Bean初始化之后执行一些初始化工作&#xff0c; 可作为一些数据的常规化…

hadoop集群部署【二】YARN MapReduce 的部署

提前注意&#xff1a;请注意路径是否和我的相同&#xff0c;放置的位置不同&#xff0c;请修改标红处 HDFS部署 HDFS介绍及部署http://t.csdnimg.cn/Q3H3Y 部署说明 Hadoop HDFS分布式文件系统&#xff0c;我们会启动&#xff1a; NameNode进程作为管理节点 DataNode进程…

WRF学习——使用CMIP6数据驱动WRF/基于ncl与vdo的CMIP6数据处理

动力降尺度 国际耦合模式比较计划&#xff08;CMIP&#xff09;为研究不同情景下的气候变化提供了大量的模拟数据&#xff0c;而在实际研究中&#xff0c;全球气候模式输出的数据空间分辨率往往较低&#xff08;>100Km&#xff0c;缺乏区域气候特征&#xff0c;为了更好地研…

K8s 集群(kubeadm) CA 证书过期解决方案

Author&#xff1a;Arsen Date&#xff1a;2024/07/04 目录 一、现象描述二、解决方案三、集群验证 一、现象描述 之前有篇文章《K8s Token 过期解决方案&#xff08;Kubeadm&#xff09;》提到了默认生成的 Token 有效期只有 24 小时&#xff0c;过期后 Token 将不可用&#…

C# 类型转换之显式和隐式

文章目录 1、显式类型转换2. 隐式类型转换3. 示例4. 类型转换的注意事项5. 类型转换的应用示例总结 在C#编程中&#xff0c;类型转换是一个核心概念&#xff0c;它允许我们在程序中处理不同类型的数据。类型转换可以分为两大类&#xff1a;显式类型转换&#xff08;Explicit Ca…

18. JAVA 多线程锁介绍

1. 前言 本节内容主要是对 Java 多线程锁进行介绍&#xff0c;是对锁的一个全方位的概述&#xff0c;为我们对后续深入学习不同的锁的使用方法奠定一个良好的基础。本节内容的知识点如下&#xff1a; 乐观锁与悲观锁的概念&#xff0c;以及两种锁之间的区别&#xff0c;这是并…

文华财经T9多空波段趋势量化交易策略模型源码

// 定义变量 Vars Numeric STEP1,MVALUE1,SARVAL,C; Numeric SARLINE,COND,ZBMA1,ZBMA2; Begin CCLOSE; STEP13/11; MVALUE120/22; SARVALSAR(4, STEP1, MVALUE1); PlotLine("",IIF(SARVAL>0,SARVAL,InvalidNumeric),RED,Circledot); PlotLine("&q…

今晚19点,《语音和心理健康》开讲!

《2024GAS声学大讲堂—音频产业创新技术公益讲座》面向医疗健康的声音与音乐技术系列专题讲座 第五讲 将于 今晚 19点 开讲&#xff0c;本次邀请了 湖南大学 教授 张子兴 演讲&#xff0c;讲座主题&#xff1a;《语音和心理健康》。此次直播方式为腾讯会议、小鹅通和中国电子音…

初出茅庐的小李博客之C语言文件操作

C语言文件操作 在C语言中&#xff0c;文件操作主要是通过标准库函数来实现的。 今天有时间就来学习下一些常用的文件操作函数&#xff1a; C 语言提供了一个 FILE 数据结构&#xff0c;记录了操作一个文件所需要的信息。该结构定义在头文件stdio.h&#xff0c;所有文件操作函…

如何通过IP地址查询地理位置及运营商信息

在数字时代&#xff0c;IP地址&#xff08;Internet Protocol Address&#xff0c;互联网协议地址&#xff09;已经成为我们日常网络活动的重要组成部分。每台连接到互联网的设备都被分配了一个唯一的IP地址&#xff0c;它不仅可以识别设备&#xff0c;还可以揭示设备的地理位置…

python数据分析入门学习笔记

目录 一、 数据分析有关的python库简介 (一)numpy (二)pandas (三)matplotlib (四)scipy (五)statsmodels (六)scikit-learn 二、 数据的导入和导出 三、 数据筛选 四、 数据描述 五、 数据处理 六、 统计分析 七、 可视化 八、 其它![](https://…

【C语言】—— 文件操作(下)

【C语言】—— 文件操作&#xff08;下&#xff09; 前言&#xff1a;五、文件的顺序读写5.1、 顺序读写函数介绍5.2、 f p u t c fputc fputc 函数5.3、 f g e t c fgetc fgetc 函数5.4、 f p u t s fputs fputs 函数5.5、 f g e t s fgets fgets 函数5.6、 f p r i n t f…

html+css+js淘宝商品界面

点击商品&#xff0c;alert弹出商品ID 图片使用了占位符图片&#xff0c;加载可能会慢一点 你可以把它换成自己的图片&#x1f603;源代码在图片后面 效果图 源代码 <!DOCTYPE html> <html lang"zh"> <head> <meta charset"UTF-8"…

Word “当前页“ 与 “前一页“ (含部分内容)间有大半页空白,删除空白方法

鼠标光标选中需要向上移的句子&#xff0c;右键点击“段落”&#xff0c;然后在跳出的窗口中按照“换行和分页”中的红色方框内取消勾选后&#xff0c;点击确定即可。

金斗云 HKMP智慧商业软件 任意用户创建漏洞复现

0x01 产品简介 金斗云智慧商业软件是一款功能强大、易于使用的智慧管理系统,通过智能化的管理工具,帮助企业实现高效经营、优化流程、降低成本,并提升客户体验。无论是珠宝门店、4S店还是其他零售、服务行业,金斗云都能提供量身定制的解决方案,助力企业实现数字化转型和智…

Proteus-51单片机-DS18B20多点测温

DS18B20多点测温 一、Proteus仿真演示 每个DS18B20都有一个唯一的64位序列号,这使得在同一总线上可以挂载多个传感器,无需额外的地址分配。主机(通常为单片机)通过特定的时序控制,可以依次读取各个DS18B20的温度数据,实现分布式测温。 二、代码特点 三、开发环境介绍 本…

【unity实战】使用unity的新输入系统InputSystem+有限状态机设计一个玩家状态机控制——实现玩家的待机 移动 闪避 连击 受击 死亡状态切换

最终效果 文章目录 最终效果前言人物素材新输入系统InputSystem的配置动画配置代码文件路径状态机脚本创建玩家不同的状态脚本玩家控制源码完结 前言 前面我们已经写过了使用有限状态机制作一个敌人AI&#xff1a;【unity实战】在Unity中使用有限状态机制作一个敌人AI 那么玩…

收银系统源码分享-PHP可二开

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物等连锁店使用。 私有化独立…

面向对象-封装

一.包 1.简介 当我们把所有的java类都写src下的第一层级&#xff0c;如果是项目中&#xff0c;也许会有几百个java文件。 src下的文件会很多&#xff0c;开发的时候不方便查找&#xff0c;也不方便维护如果较多的文件中有同名的&#xff0c;十分麻烦 模块1中有一个叫test.ja…

Nuxtjs3教程

起步 官方文档 官方目录结构 安装 npx nuxi@latest init <project-name>后面跟着提示走就行 最后yarn run dev 启动项目访问localhost:3000即可 路由组件 app.vue为项目根组件 <nuxt-page />为路由显示入口 将app.vue更改内容如下 <template><d…