【DDD】学习笔记-聚合之间的关系

聚合之间的关系

无论聚合是否表达了领域概念的完整性,我们都要清醒地认识到这种所谓的“完整”必然是相对的。如果说在领域分析模型中,每个体现了领域概念的类是模型的最小单元,那么在领域设计模型中,聚合才是模型的最小单元。我们需要一以贯之地遵守“分而治之”的思想,合理地划分聚合是“分”的体现,思考聚合之间的关系则是“合”的诉求。因此,在讨论聚合的设计过程之前,我们还需要先理清聚合之间的关系该如何设计。

论及聚合之间的关系,无非就是判断彼此之间的引用采用什么形式。分为两种:

  • 聚合根的对象引用
  • 聚合根身份标识的引用

Eric Evans 并没有规定聚合引用一定要采用什么形式,只是明确了聚合内外部之间协作的基本规则:

  • 聚合外部的对象不能引用除根实体之外的任何内部对象
  • 聚合内部的对象可以保持对其他聚合根的引用

这意味着聚合根实体可以被当前聚合的外部对象包括别的聚合的内部对象所引用。自然,无论聚合之间为何种关系,采用哪种引用方式,都需要限制聚合之间不允许出现双向导航的关系。如下图所示,聚合 A 的根实体直接访问了聚合 B 根实体的实例引用,聚合 C 的内部实体也直接访问了聚合 B 根实体的实例引用,但三者之间并没有形成双向导航:

41037969.png

如果聚合之间采用对象引用的形式,就会形成由聚合组成的对象图。由于聚合界定了边界,使得对象图的关系要更加清晰简单,对象之间的耦合强弱关系也一目了然。对象引用的形式使得从一个聚合遍历到另一个聚合非常方便,例如,当 Customer 引用了由 Order 聚合根组成的集合对象时,就可以通过 Customer 直接获得该客户所有的订单:

public class Customer extends AggregateRoot<Customer> {
    private List<Order> orders;

    public List<Order> getOrders() {
        return this.orders;
    }
}

这一实现存在的问题是:由谁负责获得当前客户的所有订单?领域驱动设计引入了资源库来管理聚合的生命周期。如果由 CustomerRepository 管理 Customer 聚合的生命周期,OrderRepository 管理 Order 聚合的生命周期,就意味着 CustomerRepository 在获得 Customer 对象的同时,还要“求助于”OrderRepository 去获得该客户的所有订单,然后将返回的订单设值给客户对象。这是何苦来由?毕竟,调用者通过向 OrderRepository 传递当前客户的身份标识 customerId,即可获得指定客户的所有订单,无需借助于 Customer 聚合根:

//client
List<Order> orders = orderRepo.allOrdersBy(customerId);

因此,一个聚合的根实体并无必要持有另一个聚合根实体的引用,若需要与之协作,可以通过该聚合根的身份标识由资源库访问获得。在分析业务场景以明确职责时,我们还需要思考究竟谁才是该职责的调用者?针对“获取客户订单”场景,表面上调用者是客户,但从分层架构的角度看,实则是由 OrderController 响应用户界面的请求而发起调用,对应的应用服务可直接通过 OrderRepository 获得客户订单:

public class OrderAppService {
    @Repository
    private OrderRepository orderRepo;

    public List<Order> customerOrders(CustomerId customerId) {
        return orderRepo.allOrdersBy(customerId);
    }
}

再来看聚合内部对象该如何引用别的聚合根。考虑 Order 聚合内 OrderItem 与 Product 之间的关系。毫无疑问,采用对象引用最为直接:

public class OrderItem extends Entity<OrderItemId> {
    // Product 为商品聚合的根实体
    private Product product;
    private Quantity quantity;

    public Product getProduct() {
        return this.product;
    }
}

如此实现,就可直接通过 OrderItem 引用的 Product 聚合根实例遍历商品信息:

List<OrderItem> orderItems = order.getOrderItems();
orderItems.forEach(oi -> System.out.println(oi.getProduct().getName());

这一实现存在同样问题:谁来负责为 OrderItem 加载 Product 聚合根的信息?OrderRepository 没有能力访问 Product 聚合,也不可能依赖 ProductRepository 来完成商品信息的加载,管理 Product 生命周期的职责也不可能交给处于 Order 聚合的内部实体 OrderItem。如果将加载的职责转移,就需要在 OrderItem 内部,引用 ProductId 而非 Product:

public class OrderItem extends Entity<OrderItemId> {
    private ProductId productId;
}

凡事有利有弊!通过身份标识引用聚合根固然解除了彼此之间强生命周期的依赖,避免了对被引用聚合对象图的加载;同时也带来了弊病:让 OrderItem 向 Product 的遍历变得复杂。怎么办?通常,我不建议将实体与值对象设计为依赖资源库的领域对象,这就意味着在 Order 聚合内部,没有 ProductRepository 这样的资源库帮助订单项根据 ProductId 去查询商品的信息。因此,若要通过 OrderItem 的 ProductId 获得商品信息,有两种方式:

  • 需要时,由调用者根据 OrderItem 包含的 ProductId 显式调用 ProductRepository,查询 Product 聚合
  • 定义 ProductInOrder 实体对象,它相当于是 Product 聚合的一个克隆或者投影,属于 Order 聚合中的内部实体,你也可以认为是分属两个限界上下文的 Product 类

第一种方式要求调用者在获得 Order 聚合并遍历内部的 OrderItem 时,每次根据 OrderItem 持有的 ProductId 获得商品信息。这个工作牵涉到聚合、资源库之间的协作,由于没有领域对象同时包含 OrderItem 与 Product,就将由数据契约对象持有它们的值,即定义 OrdersReponse。数据契约对象就是前面章节提到的 DTO 对象,该职责可以由应用服务来组装:

public class OrderAppService {
    @Repository
    private Repository orderRepository;
    @Repository
    private Repository productRepository;

    public OrdersResponse customerOrders(CustomerId customerId) {
        List<Order> orders = orderRepository.allOrdersBy(customerId);
        List<OrderResponse> orderResponses = orders.stream
                                                    .map(o -> buildFrom(o))
                                                    .collect(Collectors.toList());
        return new OrdersReponse(orderResponses);
    }

    private OrderResponse buildForm(Order order) {
        OrderResponse orderResponse = transformFrom(order);
        List<OrderItemResponse> orderItemResponses = order.getOrderItems.stream()
                                                    .map(oi -> transformFrom(oi))
                                                    .collect(Collectors.toList());
        orderResponse.addAll(orderItemResponses);
        return orderResponse;
    }
    private OrderResponse transformFrom(Order order) { ... }
    private OrderItemResponse transformFrom(OrderItem orderItem) {
        OrderItemResponse orderItemResponse = new OrderItemResponse();
        ...
        Product product = productRepository.productBy(orderItem.getProductId());
        orderItemResponse.setProductId(product.getId());
        orderItemResponse.setProductName(product.getName());
        ...        
    }
}

若担心每次根据 ProductId 查询商品信息带来可能的性能损耗,可以考虑为 ProductRepository 的实现提供缓存功能。倘若 Order 聚合与 Product 聚合属于不同的微服务(即跨进程边界的限界上下文),则查询商品信息的性能还要考虑网络通信的成本,引入缓存就更有必要了。既然 Product 聚合属于另外一个微服务,Order 与 Product 之间的协作就不再是进程内通信,也就不会直接调用 ProductRepository,而是与定义在订单微服务内的防腐层接口 ProductService 协作。该接口定义在 productcontext/interface 包中,属于当前限界上下文的南向网关。

OrderAppService 返回的 OrderResponse 对象组合了订单、订单项与商品的信息。从对象图的角度看,这三个对象之间采用的是对象引用。由于 OrderResponse 属于远程服务层或应用层的数据契约对象,因此它的设计原则和聚合的设计原则风马牛不相及,不可同等对待。

第二种方式假定了一种业务场景,即买家一旦从购物车下订单,在创建好的订单中,订单项包含的商品信息就会脱离和商品库之间的关系,无需考虑二者的同步。这时,我们可以在订单聚合中引入一个 ProductInOrder 实体类,并被 OrderItem 直接引用。ProductInOrder 的数据会持久化到订单数据库中,并与 Product 聚合根实体共享相同的 ProductId。由于 ProductInOrder 属于 Order 聚合内的实体对象,订单的资源库在管理 Order 聚合的生命周期时,会建立 OrderItem 指向 ProductInOrder 对象的导航。

社区对聚合之间的关系已有定论,皆认为聚合之间应通过身份标识进行引用。这一原则看似与面向对象设计思想相悖,毕竟面向对象正是借助对象之前的协作关系产生威力,然而,一旦对象图失去聚合边界的约束,就可能随着系统规模的扩大变成一匹脱缰的野马,难以理清楚错综复杂的对象关系。在引入聚合之后,不能将边界视为无物,而是要起到边界的保护与约束作用,这就是规定聚合协作关系的缘由。若能保证聚合之间通过身份标识而非聚合根引用进行协作,就能让聚合更好地满足完整性、独立性、不变量与事务 ACID 等本质特征。

若是在单体架构下,由于不牵涉对象之间的分布式通信,即便对象之间交织在一起,影响的仅仅是程序的逻辑架构;微服务架构则不然,若领域层的类分散在不同服务中,我们却没有定义边界去约束它们,就可能会让跨进程的对象引用变得泛滥,如果再引入事务的一致性问题,情况就变得更加严峻了。在此种情况,聚合的价值会更加凸显。

这里需要辨明聚合、限界上下文与微服务之间的关系。极端情况下,它们在逻辑上的领域边界完全重合:一个聚合就是一个限界上下文,一个限界上下文就是一个微服务。但这种一对一的映射关系并非必然,多数情况下,一个限界上下文可能包含多个聚合,一个微服务也可能包含多个限界上下文,反之,则绝对不允许一个聚合分散在不同的限界上下文,更不用说微服务了。由此就能保证同一个聚合和同一个限界上下文中的领域对象一定是在同一个进程边界内,而聚合之间的协作是否跨进程边界,又决定了事务的一致性问题。参考下图,一个限界上下文包含了两个聚合,每个聚合自有其事务边界。同一进程中的聚合 A 与聚合 B、聚合 C 与聚合 D 之间的协作可采用本地事务保证数据的强一致性;聚合 B 和聚合 C 的协作为跨进程通信,需要采用柔性事务保证数据的最终一致性:

75740194.png

因此,聚合之间通过身份标识进行引用,可以避免跨进程边界的对象引用,而聚合边界与进程边界又共同决定了事务的处理方式。这是一种设计约束,表面看来,它似乎给领域设计模型带上了镣铐,让模型对象之间的协作变得不那么简单直接,带来的价值却是让领域设计模型变得更加清晰、可控且纯粹。倘若系统为单体架构,若在设计时严格按照这一设计约束引入了聚合,当未来需要迁移到微服务架构时,也将因为聚合而降低重构或重写的成本。从这个角度讲,说聚合是领域驱动战术设计中最为重要的设计要素也不为过。

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

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

相关文章

枚举(C/C++)

没有什么成套的算法&#xff0c;直接上例题&#xff01;&#xff01; 例题1&#xff1a;赢球票 代码&#xff1a; #include <bits/stdc.h> using namespace std;const int maxn 105; int n,num1[maxn],num2[maxn],cnt,cnt1,sum,ans;int check1()//检查剩余个数 {cnt1…

ZYNQ:PL-CAN总线功能应用

流程背景 前期基本实现PS端的CAN总线功能&#xff0c;现阶段的主要目的是实现PL端的CAN总线功能&#xff0c;需要采用CAN IP。 PL系统搭建 PL外设时钟源 搭建完vivado系统后&#xff0c;需要在sdk编程。但是在配置PL&#xff0d;CAN时&#xff0c;意识到CAN时钟值不清楚&…

TIM输出比较 P2

D触发器&#xff1f; 一、输出比较 二、PWM 1、简介 2、结构 三、外部设备 1.舵机 2.直流电机 我的理解是xO1 xIN1 & PWMx; xO2 xIN2 & PWMx;引入PWMx可以更方便的控制特定的电路。 四、函数学习 /*****单独设置输出比较极性*****/ void TIM_OC1PolarityConfig(…

php基础学习之可变函数(web渗透测试关键字绕过rce和回调函数)

可变函数 看可变函数的知识点之前&#xff0c;蒟蒻博主建议你先去看看php的可变变量&#xff0c;会更加方便理解&#xff0c;在本篇博客中的第五块知识点->php基础学习之变量-CSDN博客 描述 当一个变量所保存的值刚好是一个函数的名字&#xff08;由函数命名规则可知该值必…

JavaScript中什么是事件委托

JavaScript 中的事件委托&#xff08;Event delegation&#xff09;是一种重要的编程技术&#xff0c;它能够优化网页中的事件处理&#xff0c;提高程序的性能和可维护性。本文将详细介绍事件委托的概念、工作原理&#xff0c;并提供示例代码来说明其实际应用。 事件委托是基于…

我的NPI项目之Android USB 系列(一) - USB的发展历史

设计目的 USB was designed to standardize the connection of peripherals to personal computers, both to exchange data and to supply electric power. 一个是为了标准化电脑连接外设的方法。 能够支持电脑和外设的数据交互和&#xff08;对外&#xff09;供电。 目前已…

leetcode:96.不同的二叉搜索树

解题思路&#xff1a; 输入n3 n 0 1个 n 1 1个 n 2 2个 头1头2头3 头1 左子树0节点&#xff08;个数&#xff09;x右子树2个节点&#xff08;个数&#xff09; 头2 左子树1节点&#xff08;个数&#xff09;x右子树1个节点&#xff08;个数&#xff09; 头3 左子…

【Java程序员面试专栏 分布式中间件】Redis 核心面试指引

关于Redis部分的核心知识进行一网打尽,包括Redis的基本概念,基本架构,工作流程,存储机制等,通过一篇文章串联面试重点,并且帮助加强日常基础知识的理解,全局思维导图如下所示 基础概念 明确redis的特性、应用场景和数据结构 什么是Redis,Redis有哪些应用场景 Redi…

windows 下跑起大模型(llama)操作笔记

原贴地址&#xff1a;https://testerhome.com/topics/39091 前言 国内访问 chatgpt 太麻烦了&#xff0c;还是本地自己搭一个比较快&#xff0c;也方便后续修改微调啥的。 之前 llama 刚出来的时候在 mac 上试了下&#xff0c;也在 windows 上用 conda 折腾过&#xff0c;环…

ubuntu22.04@laptop OpenCV Get Started: 011_edge_detection

ubuntu22.04laptop OpenCV Get Started: 011_edge_detection 1. 源由2. edge_detection应用Demo2.1 C应用Demo2.2 Python应用Demo 3. 重点逐步分析3.1 GaussianBlur去噪3.2 Sobel边缘检测3.2.1 SobelX方向边缘检测3.2.2 SobelY方向边缘检测3.2.3 SobelXY方向边缘检测 3.3 Canny…

写一个程序,输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数输入-1表示结束

#include <stdio.h> int main(void) {int x;int count[10];int i;for(i0;i<10;i){//初始化数组 count[i]0;}scanf("%d",&x);while(x!-1){if( x>0 && x<9){count[x];//数组参与运算 }scanf("%d",&x);}for(i0;i<10;i){pr…

云计算基础-大页内存

大页内存功能概述 什么是大页内存 简单来说&#xff0c;就是通过增大操作系统页的大小来减小页表&#xff0c;从而避免快表缺失 主要应用场景 主要运用于内存密集型业务的虚拟机&#xff0c;比如对于运行数据库系统的虚拟机&#xff0c;采用HugePages(大页)后&#xff0c;可…

下一代模型:Gemini 1.5,正如它的名字一样闪亮登场

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Leetcode 145.二叉树的后序遍历

题目 给你一棵二叉树的根节点 root &#xff0c;返回其节点值的 后序遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[3,2,1] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#xff1a;root…

SQL中的各种连接的区别总结

前言 今天主要的内容是要讲解SQL中关于Join、Inner Join、Left Join、Right Join、Full Join、On、 Where区别和用法&#xff0c;不用我说其实前面的这些基本SQL语法各位攻城狮基本上都用过。但是往往我们可能用的比较多的也就是左右连接和内连接了&#xff0c;而且对于许多初学…

STM32 HAL库 STM32CubeMX -- IWDG(独立看门狗)

STM32 HAL库 STM32CubeMX -- IWDG 一、IWDG简介二、独立看门狗的工作原理三、驱动函数初始化函数HAL IWDG Init()初始化函数HAL IWDG Init()其他宏函数 四、超时时间计算第一种办法第二种办法&#xff08;推荐&#xff09; 一、IWDG简介 看门狗(Watchdog)就是MCU上的一种特殊的…

悦纳自己:拥抱个人局限,开启成长之旅

悦纳自己&#xff1a;拥抱个人局限&#xff0c;开启成长之旅 在人生的旅途中&#xff0c;我们每个人都会面临无数的挑战和选择。有时我们会因为这些挑战而感到焦虑和不安&#xff0c;因为我们害怕失败&#xff0c;害怕无法达到预期的目标。然而&#xff0c;真正重要的是我们如何…

前端开发:Vue框架与前端部署

Vue Vue是一套前端框架&#xff0c;免除原生)avaScript中的DOM操作&#xff0c;简化书写。是基于MVVM(Model–View-ViewModel)思想&#xff0c;实现数据的双向绑定&#xff0c;将编程的关注点放在数据上。简单来说&#xff0c;就是数据变化的时候, 页面会自动刷新, 页面变化的时…

leetcode hot100爬楼梯

在本题目中&#xff0c;要求爬第n阶有多少种爬法&#xff0c;并且每次只能爬1个或者2个&#xff0c;这明显是动态规划的问题&#xff0c;我们需要用动态规划的解决方式去处理问题。动态规划就是按照正常的顺序由前向后依次推导。而递归则是从结果往前去寻找&#xff08;个人理解…

【打工日常】使用docker部署可视化工具docker-ui

一、docker-ui介绍 docker-ui是一个易用且轻量化的Docker管理工具&#xff0c;透过Web界面的操作&#xff0c;方便快捷操作docker容器化工作。 docker-ui拥有易操作化化界面&#xff0c;不须记忆docker指令&#xff0c;仅需下载镜像即可立刻加入完成部署。基于docker的特性&…