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;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(contentUrl);}
    }
    
结论

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Git 学习笔记】gitk 命令与 git log 其他参数的使用

1.7 用 gitk 查看提交历史 # make sure you have gitk installed $ which gitk /usr/bin/gitk # Sync the commit ID $ git checkout master && git reset --hard 13dcad # bring up the gitk interface, --all to see everything $ gitk --all &实测结果&#xf…

为什么salesforce需要设置社区端,而不是使用和内部员工同样的环境

虽然企业可能希望为客户和合作伙伴提供与内部员工相同的环境&#xff0c;但实际上这样做有几个显著的缺点和风险。这些包括&#xff1a; 安全性和权限管理&#xff1a;内部员工的系统通常包含敏感和机密的信息&#xff0c;例如财务数据、内部策略和未发布的产品信息。将客户和合…

速速来get新妙招!苹果手机护眼模式在哪里开启

在日常生活中&#xff0c;我们经常长时间使用手机&#xff0c;无论是工作还是娱乐&#xff0c;屏幕的蓝光都会对眼睛造成一定的伤害。为了减轻眼睛疲劳&#xff0c;苹果手机推出了护眼模式&#xff0c;也叫“夜览”模式&#xff0c;通过调整屏幕色温&#xff0c;让显示效果更温…

MySQL 8.0 架构 之 中继日志(Relay log)

文章目录 MySQL 8.0 架构 之 中继日志&#xff08;Relay log&#xff09;中继日志&#xff08;Relay log&#xff09;概述相关参数参考 【声明】文章仅供学习交流&#xff0c;观点代表个人&#xff0c;与任何公司无关。 来源|WaltSQL和数据库技术(ID:SQLplusDB) MySQL 8.0 OCP …

PyTorch - 神经网络基础

神经网络的主要原理包括一组基本元素&#xff0c;即人工神经元或感知器。它包括几个基本输入&#xff0c;例如 x1、x2… xn &#xff0c;如果总和大于激活电位&#xff0c;则会产生二进制输出。 样本神经元的示意图如下所述。 产生的输出可以被认为是具有激活电位或偏差的加权…

四、(3)补充beautifulsoup、re正则表达式、标签解析

四、&#xff08;3&#xff09;补充beautifulsoup、re正则表达式、标签解析 beautifulsoupre正则表达式正则提取标签解析 beautifulsoup 补充关于解析的知识 还需要看爬虫课件 如何定位文本或者标签&#xff0c;是整个爬虫中非常重要的能力 无论find_all&#xff08;&#xff…

Spring启动时,将SpringContext设置到Util中(SpringContextUtil)

场景 在Spring应用开发中&#xff0c;为简化代码或者在静态方法中获取Spring应用的上下文&#xff0c;需要把SpringContext设置到类属性上。经过对源码的分析和实践&#xff0c;使用Spring的事件监听器监听ApplicationPreparedEvent事件是最佳的方式。 通过ApplicationPrepar…

matrixone集群搭建、启停、高可用扩缩容和连接数据库

1. 部署 Kubernetes 集群 由于 MatrixOne 的分布式部署依赖于 Kubernetes 集群&#xff0c;因此我们需要一个 Kubernetes 集群。本篇文章将指导你通过使用 Kuboard-Spray 的方式搭建一个 Kubernetes 集群。 准备集群环境 对于集群环境&#xff0c;需要做如下准备&#xff1a…

mysql在windows下的安装

一&#xff0c;软件安装 只修改开头的系统盘 二&#xff0c;环境变量配置 找到MySQL安装目录对应的bin目录复制路径粘贴过来 三&#xff0c;cmd

SSL/CA 证书及其相关证书文件解析

在当今数字化的时代&#xff0c;网络安全变得至关重要。SSL&#xff08;Secure Socket Layer&#xff09;证书和CA&#xff08;Certificate Authority&#xff09;证书作为保护网络通信安全的重要工具&#xff0c;发挥着关键作用。 一、SSL证书 SSL证书是数字证书的一种&…

SSM少儿读者交流系-计算机毕业设计源码20005

摘要 随着信息技术的发展和互联网的普及&#xff0c;少儿读者之间的交流方式发生了革命性的变化。通过使用Java编程语言&#xff0c;可以实现系统的高度灵活性和可扩展性。而SSM框架的采用&#xff0c;可以提供良好的开发结构和代码管理&#xff0c;使系统更加稳定和易于维护。…

同方威视受邀盛装亮相2024长三角快递物流展(杭州)助力行业物畅其流

同方威视技术股份有限公司携安全检测产品和综合解决方案&#xff0c;盛装亮相2024长三角快递物流展&#xff08;杭州&#xff09; 展位号&#xff1a;3C馆A07-1 时间&#xff1a;2024年7月8-10日 地址&#xff1a;杭州国际博览中心&#xff08;浙江省杭州市萧山区奔竞大道35…

【CSAPP】-linklab实验

目录 实验目的与要求 实验原理与内容 实验步骤 实验设备与软件环境 实验过程与结果&#xff08;可贴图&#xff09; 实验总结 实验目的与要求 1.了解链接的基本概念和链接过程所要完成的任务。 2.理解ELF目标代码和目标代码文件的基本概念和基本构成 3.了解ELF可重定位目…

STM32F1+HAL库+FreeTOTS学习2——STM32移植FreeRTOS

STM32F1HAL库FreeTOTS学习2——STM32移植FreeRTOS 获取FreeRTOS源码创建工程窥探源码移植 上期我们认识了FreeRTOS&#xff0c;对FreeRTOS有了个初步的认识&#xff0c;这一期我们来上手移植FreeRTOS到STM32上。 获取FreeRTOS源码 进入官网&#xff1a;https://www.freertos.o…

故障转移处理器的工作原理

将故障的sink降级到故障池中&#xff0c;并在池中为它们分配一个冷却器&#xff0c;在重试之前&#xff0c;故障时间会增加&#xff0c;当sink成功发送event后&#xff0c;它将恢复到活跃池中。sink具有与之相关的优先级&#xff0c;数值越大&#xff0c;优先级越高。如果在发送…

Frrouting快速入门——OSPF组网(一)

FRR简介 FRR是FRRouting的简称&#xff0c;是一个开源的路由交换软件套件。其作者源自老牌项目quaga的成员&#xff0c;也可以算是quaga的新版本。 使用时一般查看此文档&#xff1a;https://docs.frrouting.org/projects/dev-guide/en/latest/index.html FRR支持的协议众多…

网络爬虫(一)深度优先爬虫与广度优先爬虫

1. 深度优先爬虫&#xff1a;深度优先爬虫是一种以深度为优先的爬虫算法。它从一个起始点开始&#xff0c;先访问一个链接&#xff0c;然后再访问该链接下的链接&#xff0c;一直深入地访问直到无法再继续深入为止。然后回溯到上一个链接&#xff0c;再继续深入访问下一个未被访…

AOP切面、动态代理

目录 一、用切面做的是怎么声明切面的&#xff1f;&#xff08;切面是怎么用的&#xff1f;&#xff09; 二、切点是怎么定义的&#xff1f; 三、说一下面向切面编程&#xff1f; 四、AOP是怎么实现的&#xff1f; 五、说一下动态代理&#xff1f; 一、用切面做的是怎么声明切面…

promise.all和promise.race的区别

Promise.all和Promise.race是JavaScript中Promise API的两个重要方法&#xff0c;它们在处理多个Promise对象时表现出不同的行为。以下是它们之间的主要区别&#xff1a; 1. 功能和行为 Promise.all&#xff1a; 功能&#xff1a;接收一个包含多个Promise的数组&#x…

【leetcode--逆波兰表达式】

今天学逆波兰表达式撰写还顺便复习了一下二叉树的前中后序遍历&#xff1a; 前序遍历&#xff1a;根左右 中序遍历&#xff1a;左根右 后序遍历&#xff1a;左右根 本题两个要点&#xff1a; a.判断字符串中的元素是数字还是字符串 因为Python 中没有一个函数可以判断一个字…