DDD系列:三、Repository模式

news2024/11/26 4:01:10

为什么需要Repository?

Anemic Domain Model(贫血领域模型)特征:

  1. 有大量的XxxDO对象:这里DO虽然有时候代表了Domain Object,但实际上仅仅是数据库表结构的映射,里面没有包含(或包含了很少的)业务逻辑;
  2. 服务和Controller里有大量的业务逻辑:比如校验逻辑、计算逻辑、格式转化逻辑、对象关系逻辑、数据存储逻辑等;
  3. 大量的Utils工具类等。

Anemic Domain Model的缺陷:

  1. 无法保护模型对象的完整性和一致性:因为对象的所有属性都是公开的,只能由调用方来维护模型的一致性,而这个是没有保障的;之前曾经出现的案例就是调用方没有能维护模型数据的一致性,导致脏数据使用时出现bug,这一类的bug还特别隐蔽,很难排查到。
  2. **对象操作的可发现性极差:**单纯从对象的属性上很难看出来都有哪些业务逻辑,什么时候可以被调用,以及可以赋值的边界是什么;比如说,Long类型的值是否可以是0或者负数?
  3. **代码逻辑容易重复:**比如校验逻辑、计算逻辑,都很容易出现在多个服务、多个代码块里,提升维护成本和bug出现的概率;一类常见的bug就是当贫血模型变更后,校验逻辑由于出现在多个地方,没有能跟着变,导致校验失败或失效。
  4. 代码的健壮性差:比如一个数据模型的变化可能导致从上到下的所有代码的变更。

充血模型中,需要严格区分的两个模型:

  • 数据模型(Data Model):指业务数据该如何持久化,以及数据之间的关系,也就是传统的ER模型。只属于Infrastructure Layer
  • 业务模型/领域模型(Domain Model):指业务逻辑中,相关联的数据该如何联动。只属于Domain Layer

链接了这两层的关键对象,就是Repository。

Repository的价值

​ 在传统的数据库驱动开发中,我们会对数据库操作做一个封装,一般叫做Data Access Object(DAO)。DAO的核心价值是封装了拼接SQL、维护数据库连接、事务等琐碎的底层逻辑,让业务开发可以专注于写代码。但是在本质上,DAO的操作还是数据库操作,DAO的某个方法还是在直接操作数据库和数据模型,只是少写了部分代码。

​ 所以,我们希望将我们的 业务逻辑 和 DB相关的操作(DAO、DB)隔离开,让我们的业务逻辑,不关心如何数据如何落库或查找

模型对象代码规范

模型类型:

3种模型的区别,Entity、Data Object (DO)和Data Transfer Object (DTO):

  • DO(数据对象)

    ​ 在DDD的规范里,DO应该仅仅作为数据库物理表格的映射,不能参与到业务逻辑中。为了简单明了,DO的字段类型和名称应该和DB中的字段类型和名称一一对应。(当然,实际上也没必要一摸一样,只要你在Mapper那一层做到字段映射)

  • Entity(实体对象)

    ​ 实体对象是我们正常业务应该用的业务模型,它的字段和方法应该和业务语言保持一致,和持久化方式无关。也就是说,Entity 和 DO 很可能有着完全不一样的字段命名和字段类型,甚至嵌套关系。Entity的生命周期应该仅存在于内存中,不需要可序列化和可持久化

  • DTO(传输对象):

    ​ 主要作为Application Layer 的入参和出参,比如CQRS里的Command、Query、Event,以及Request、Response等都属于DTO的范畴。DTO的价值在于适配不同的业务场景的入参和出参,避免让业务对象变成一个万能大对象。该对象需要序列化,其余两模型都不能实现序列化接口。

模型对象间的关系:

  • 复杂的Entity拆分多个DO对象
  • 多个关联的 Entity 合并一个 DO
  • 从复杂 Entity 里抽取部分信息形成一个 DTO 列表
  • 从多个 Entity 中提取信息,输出一个 DTO

模型转化器

请添加图片描述

DTO Assembler:

​ 在 Application 层,Entity 与 DTO 之间的转化器有一个标准的名称叫DTO Assembler,他的核心作用就是将1个或多个相关联的Entity转化为1个或多个DTO。

Data Converter:

​ 在Infrastructure层,Entity 与 DO 的转化器没有一个标准名称,但是为了区分 Data Mapper(Mybatis的Mapper),我们叫这种转化器Data Converter。

带来的好处:

​ 通过抽象出一个 Assembler/Converter 对象,把复杂的转化逻辑都收敛到一个对象中,并且可以很好的进行单元测试。

带来的问题:

  • 当业务复杂时,手写 Assembler/Converter 是一件耗时且容易出bug的事情,所以业界会有多种Bean Mapping的解决方案,从本质上分为动态和静态映射。
    • 动态映射根据反射动态赋值,大量的反射调用,将带来性能问题。
    • 推荐使用 MapStruct(MapStruct官网),该组件通过注解,在编译时静态生成映射代码,其最终编译出来的代码和手写的代码在性能上完全一致,且有强大的注解等能力。
  • 从使用复杂度角度来看,区分了DO、Entity、DTO带来了代码量的膨胀(从1个变成了3+2+N个)。但是在实际复杂业务场景下,通过功能来区分模型带来的价值是功能性的单一和可测试、可预期,最终反而是逻辑复杂性的降低。

Repository代码规范

​ Repository 出现的目的,是为了将 业务逻辑(软件) 与 硬件(DB、Cache、文件系统等)和 固件(与硬件强关联的软件)完全隔离开。所以为了体现出软件的特点,Repository 中需要注意以下三点:

  1. **接口名称不应该使用底层实现的语法:**常见的insertselectupdatedelete都属于SQL语法,使用这几个词相当于和DB底层实现做了绑定,在 Repository 为了区分开,我们使用语法如findsaveinsertupdate)、remove
  2. **出参入参不应该使用底层数据格式:**Repository 操作的是 Entity 对象(实际上应该是Aggregate Root),不应该直接操作 DO。Repository 接口存在于 Domain 层,实现类 建议 在 Infrastructure 层。
  3. **应该避免所谓的“通用”Repository模式:**很多ORM框架都提供一个“通用”的Repository接口,然后框架通过注解自动实现接口,比较典型的例子是Spring Data、Entity Framework等,这种框架的好处是在简单场景下很容易通过配置实现,但是坏处是基本上无扩展的可能性(比如加定制缓存逻辑),在未来有可能还是会被推翻重做。当然,这里避免通用不代表不能有基础接口和通用的帮助类。

复杂Aggregate的save

​ 在对一个复杂Aggregate(包含多个Entity)的 save 操作中,并不是所有 Aggregate 里的 Entity 都需要变更。

​ 如果不知道 Aggregate 中的变更有哪些,那么只有全量更新,导致大量的无用DB操作;如果我们可以追踪到变更的 Entity,那么将可以减少很多无用的 DB 操作。

业界目前有两个主流的变更追踪方案:

  1. 基于Snapshot的方案:当数据从DB里取出来后,在内存中保存一份snapshot,然后在数据写入时和snapshot比较。常见的实现如Hibernate
  2. 基于Proxy的方案:当数据从DB里取出来后,通过weaving的方式将所有setter都增加一个切面来判断setter是否被调用以及值是否变更,如果变更则标记为Dirty。在保存时根据Dirty判断是否需要更新。常见的实现如Entity Framework。

方案对比:

Snapshot(推荐)Proxy
复杂程度通过cache实现,使用流程较简单通过代理标记实现,工具类较复杂,使用流程简单
内存消耗无额外内存占用
带来的问题cache与DB的数据一致性问题代理标记Dirty时,对复杂Aggregate的判断问题

Repository的迁移流程

​ 使用 Repository 模式最大的收益就是可以彻底和底层固件解耦,让上层业务可以快速自发展。

假设传统方式下,查询数据只有如下两个类:

  • OrderDO:和DB一样的数据结构
  • OrderDAO:和DB交互的业务操作类

其升级流程如下:

  1. 生成Order实体类,初期字段可以和OrderDO保持一致
  2. 生成OrderDataConverter,通过MapStruct(Java Bean转换器)基本上2行代码就能完成
  3. 写单元测试,确保Order和OrderDO之间的转化100%正确
  4. 生成OrderRepository接口和实现,通过单测确保OrderRepository的正确性
  5. 将原有代码里使用了OrderDO的地方改为Order
  6. 将原有代码里使用了OrderDAO的地方都改为用OrderRepository
  7. 通过单测确保业务逻辑的一致性。

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

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

相关文章

kafka整理

kafka整理 一、kafka概述 kafka是apache旗下一款开源的顶级的消息队列的系统, 最早是来源于领英, 后期将其贡献给apache, 采用语言是scala.基于zookeeper, 启动kafka集群需要先启动zookeeper集群, 同时在zookeeper记录kafka相关的元数据 kafka本质上就是消息队列的中间件产品…

Codeforces Round 867 (Div. 3)(A-G2)

文章目录 A. TubeTube Feed1、题目2、分析3、代码, B. Karina and Array1、题目2、分析3、代码 C. Bun Lover1、问题2、分析(1)观察样例法(2)正解推导 3、代码 D. Super-Permutation1、问题2、分析(1&#…

力扣第343场周赛

第一次力扣,等大二寒暑假,有时间再来系统刷题 目录 🌼前言 🌼一,6341.保龄球游戏的获胜者 🌼二,6342.找出叠涂元素 🌳第一次 -- 超时 🌳第二次 -- AC &#x1f33c…

二叉树相关的简单递归oj

二叉树相关的简单递归oj 前言题目二叉树的前序遍历相同的树判断单值二叉树对称二叉树另一棵树的子树创建二叉树并遍历 前言 这篇博客主要是博主感觉对二叉树oj题目不太熟悉,随便整理的一下题目和解答,方便复习,所以讲题部分主要以我自己以及为…

Java 基础入门篇(二)——— Java 基础语法

文章目录 一、注释二、字面量三、变量3.1 变量概述3.2 变量在计算机中的底层原理 四、数据类型五、关键字、标志符六、类型转换6.1 自动类型转换6.2 表达式的自动类型转换6.3 强制类型转换 七、运算符7.1 基本算数运算符7.2 符号做连接符7.3 自增自减运算符7.4 赋值运算符7.5 …

【C++技能树】类的六个成员函数Ⅰ --构造、析构、拷贝构造函数

Halo,这里是Ppeua。平时主要更新C语言,C,数据结构算法…感兴趣就关注我吧!你定不会失望。 本篇导航 0.this指针1.Class默认成员函数2.构造函数调用规则: 3.析构函数4.拷贝构造函数 0.this指针 在开始本章内容之前,先浅…

Channel-wise Knowledge Distillation for Dense Prediction(ICCV 2021)原理与代码解析

paper:Channel-wise Knowledge Distillation for Dense Prediction official implementation:https://github.com/irfanICMLL/TorchDistiller/tree/main/SemSeg-distill 摘要 之前大多数用于密集预测dense prediction任务的蒸馏方法在空间域spatial…

(求正数数组的最小不可组成和,养兔子)笔试强训

博主简介:想进大厂的打工人博主主页:xyk:所属专栏: JavaEE初阶 目录 文章目录 一、选择题1 二、[编程题]养兔子 三、[编程题]求正数数组的最小不可组成和 一、选择题1 reflection是如何工作的__牛客网 (nowcoder.com) 考虑下面这个简单的例子&…

大数据Doris(八):Broker部署和集群启停脚本

文章目录 Broker部署和集群启停脚本 一、Broker部署 1、准备Broker 安装包 2、启动 Broker

PyQt6剑指未来-日期和时间

前言 时间和日期是软件开发中非常重要的概念。在PyQt6中,时间和日期模块提供了处理日期、时间和日期时间的类和函数,以及管理时区和夏令时的特性。这些模块提供了可靠和易于使用的工具,使得在PyQt6中处理和呈现时间和日期的操作变得轻松起来…

Java中Lambda表达式(初学到精通)

目录 一、Lambda表达式是什么?什么场景下使用Lambda? 1.Lambda 表达式是什么 2.函数式接口是什么 第二章、怎么用Lambda 1.必须有一个函数式接口 2.省略规则 3.Lambda经常用来和匿名内部类比较 第三章、具体使用举例() 1.案…

跳跃游戏类题目 总结篇

一.跳跃游戏类题目简单介绍 跳跃游戏是一种典型的算法题目,经常是给定一数组arr,从数组的某一位置i出发,根据一定的跳跃规则,比如从i位置能跳arr[i]步,或者小于arr[i]步,或者固定步数,直到到达某…

C++ 链表概述

背景 当需要存储大量数据并需要对其进行操作时,常常需要使用到链表这种数据结构。它可以用来存储一系列的元素并支持插入、删除、遍历等操作。 概念 一般来说,链表是由若干个节点组成的,每个节点包含了两个部分的内容:存储的数…

【嵌入式环境下linux内核及驱动学习笔记-(6-内核 I/O)-阻塞与非阻塞】

目录 1、阻塞与非阻塞1.1 以对recvfrom函数的调用及执行过程来说明阻塞的操作。1.2 以对recvfrom函数的不断轮询调用为例,说明非阻塞时进程的行为。1.3 简单介绍内核链表及等待队列1.4 等待队列1.4.1 定义等待队列头部(wait_queue_head_t)1.4…

vue动态添加多组数据添加正则限制

如图新增多条数据,如果删除其中一条正则校验失败的数据,提示不会随之删除,若想提示删除并不清空数据 delete (item, index) {this.applicationForm.reserveInfo.forEach((v, i) > {if (i index) {this.$refs.formValidate.fields.forEac…

UFT——操作模块

示例一 创建一个可重复利用的登录测试更改Action的名称。使用本地数据表。创建一个主调用测试。建立测试迭代。处理缺失的Action。 分析:就是创建一个只有登录的测试起名为login,然后在创建一个主测试起名字比如main,在main中,调用…

微信小程序定义模板

微信小程序提供模板(template)功能,把一些可以共用的,复用的代码在模板中定义为代码片段,然后在不同的地方调用,可以实现一次编写,多次引用的效果。 首先我们看一下官网是如何操作的 一般的情…

笔记:对多维torch进行任意维度的多“行”操作

如何取出多维torch指定维度的指定“行” 从二维torch开始新建torch取出某一行取出某一列一次性取出多行取出连续的多行取出不连续的多行 一次取出多列取出连续的多列取出不连续的多列 考虑三维torch取出三维torch的任意两行(means 在dim0上操作)取出连续…

( 字符串) 9. 回文数 ——【Leetcode每日一题】

❓9. 回文数 难度:简单 给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。 回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。 例如…