技术

Python ioc 从0到1构建一个db 上下文记忆 agentic chat 图数据库的一些考量 LLM一些探索 Agent实践 LLM预训练 向量数据库的一些考量 fastapi+sqlalchemy进行项目开发 LLM微调实践 Python协程实现 Agent Functon Calling LLamaIndex入门 Multi-Agent探索 Python虚拟机 LLM工作流编排 Python实践 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 Transformers源码学习 LangChain源码学习 通用分布式计算引擎Ray Python并发 go依赖注入 go collection gc的基本原理 golang性能分析及优化 数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 Kubernetes 控制器模型 容器日志采集 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 线程排队 jib源码分析之细节 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

bert rerank微调 大模型推理tips RAG向量检索与微调 dddfirework源码分析 RAG与知识图谱 大模型推理服务框架vLLM 大模型推理服务框架 模型服务化(未完成) 大模型Post-Training 大模型训练 大模型推理 从Attention到Transformer k8s设备管理 ddd从理念到代码 如何应用LLM 小鼠如何驾驭大象(LLM)? 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 kubevela源码分析 容器和CPU那些事儿 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 K8S YAML 资源清单管理方案 tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 推荐系统embedding原理及实践 机器学习中的python调用c 机器学习训练框架概述 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tensorflow学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Admission Controller 与 Admission Webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI——kubelet与容器引擎之间的接口 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 kubevela源码分析 容器和CPU那些事儿 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 K8S YAML 资源清单管理方案 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI——kubelet与容器引擎之间的接口 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 什么是云原生 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件
上下文记忆 agentic chat bert rerank微调 大模型推理tips LLM一些探索 Agent实践 LLM预训练 RAG向量检索与微调 LLM微调实践 RAG与知识图谱 大模型推理服务框架vLLM Agent Functon Calling LLamaIndex入门 Multi-Agent探索 LLM工作流编排 大模型推理服务框架 模型服务化(未完成) 大模型Post-Training 大模型训练 大模型推理 从Attention到Transformer 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Transformers源码学习 LangChain源码学习 如何应用LLM 小鼠如何驾驭大象(LLM)? AutoML和AutoDL 特征平台 实时训练 tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 推荐系统embedding原理及实践 机器学习中的python调用c 机器学习训练框架概述 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 推理服务 mpi 学习pytorch 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tensorflow学习 kaggle泰坦尼克问题实践 神经网络模型优化 概率论 直觉上理解深度学习 如何学习机器学习 深度学习泛谈

ddd从理念到代码

2023年07月06日

简介

如果我们不做工程,只是简单的写一个程序,我们都可以很熟练的使用面向对象,但就是因为工程的复杂性,导致我们没有办法随心所欲去用面向对象里的各种优秀设计。

理念

How To Implement Domain-Driven Design (DDD) in Golang Domain-Driven Design is a way of structuring and modeling the software after the Domain it belongs to. What this means is that a domain first has to be considered for the software that is written. The domain is the topic or problem that the software intends to work on. The software should be written to reflect the domain. The architecture in the code should also reflect on the domain.

迄今为止最完整的DDD实践一些套话

  1. 边界清晰的设计方法:通过领域划分,识别哪些需求应该在哪些领域,不断拉齐团队对需求的认知,分而治之,控制规模。
  2. 统一语言:团队在有边界的上下文中有意识地形成对事物进行统一的描述,形成统一的概念(模型)。
  3. 业务领域的知识沉淀:通过反复论证和提炼模型,使得模型必须与业务的真实世界保持一致。促使知识(模型)可以很好地传递和维护。
  4. 面向业务建模:领域模型与数据模型分离,业务复杂度和技术复杂度分离。

设计

从概念开始理解DDD

聚合根、领域对象、领域服务、领域事件、仓储、贫血充血模型、界限上下文、通用语言

从分层开始理解DDD

一文详谈领域驱动设计实践我们工作的代码库中最多类名后缀就是Service了,我们也应该被各种Service的调用层级、循环依赖教训过很多遍了,出现这种问题实际上还是我们对代码缺少设计,一股脑的把业务逻辑、系统逻辑、应用逻辑、基础设施等等随意组装到一起使用,本来每一个部分的复杂度就已经非常高了,我们还要将这些复杂度揉到一起。领域驱动设计给我们提供了一种分层治理的思路,将系统内的服务类分为几个大类:应用层服务、领域层服务、基础设施层服务。

  1. 应用层服务用于处理输入输出、与领域模型和领域服务之间的调度、连接基础设施层服务。
    1. 在将领域模型与工程结合的过程中,应用服务(ApplicationService)扮演了十分重要的角色,它对入口、领域模型、外部依赖、基础设施等部分进行编排和调度,最终使领域模型能够在实际应用中正常工作。
  2. 当领域模型中某个动作或者操作不能看作某个领域对象自身的职责时,可以将其托管到一个单独的服务类中,这种服务类,我们把它叫做领域服务。对于领域服务的使用,经常很难去定义哪些行为或逻辑是应该托管到服务类中还是由领域对象自己来负责。全部托管到领域服务中,领域对象则会变成贫血模型,如果不托管,又容易因职责过多而导致领域对象过于膨胀。对于这个问题我们也没有太好的解决办法,软件工程的问题永远都是在Balance的过程中,当代码复杂度可控的范围内,我们尽量减少对领域服务的使用,如果领域对象开始出现膨胀的现象,那就将其托管到领域服务中。对于领域服务,一定要把守住一条底线,领域服务一定不要有状态,也就是我们所说的“纯函数”,这样做能够让领域服务保持单纯,仅关注于领域对象之间的关系和其状态的变化,而不会引入领域逻辑以外的复杂性。

https://github.com/KendoCross/kendoDDD 概念太多了,换一个角度,从MVC/MVP/MVVM的分层架构入手,类比理解DDD经典的四层。然后融合自己已有的编码习惯和认知,按照各层的主要功能定位,可以写的代码范围约束,慢慢再结合理解DDD的概念完善代码编写。

分层,分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。

  1. 严格分层架构,某层只能与直接位于其下方的层发生耦合;自然是最理想化的,但这样肯定会导致大量繁琐的适配代码出现,故在严格与松散之间,追寻和把握恰达好处的均衡。
  2. 松散分层架构,则允许任意上方层与任意下方层发生耦合。

各层理解

  1. 用户接口层/Presentation。负责向用户显示信息和解释用户指令
    1. 出入口主要的功用逻辑也尽量的简单,主要承接不同“表现”形式采集到的指令/出入参等,并进行转发给应用层。和不同的Web/RPC框架有一定的耦合,不同的框架代码不全一样。
    2. 该层的核心价值在于多样化,而不在于功能有多强大,不涉及到具体的业务逻辑
  2. 应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。只能做简单的数据转换 & 触发,转换成能处理的内部指令,交给 entity 执行
  3. 应用层要尽量简单,不包含业务规则,只为下一层中的领域对象协调任务,分配工作。应用层是很薄的一层,只作为计算机领域到业务领域的过渡层。
  4. 这一层直接消费领域层,并且开始记录一些系统型功能,比如运行日志、事件溯源。 这一层的也应该尽可能的业务无关,以公用代码逻辑为主。
  5. 通过直接持有领域层的聚合根,infra层等直接进行业务表达。并将不常变化的domain model,转换为可能经常变化的view model。
  6. 领域层。负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节由基础设施层提供,但反应业务情况的状态是由本层控制并使用的。 在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。
    1. 可以细拆分为聚合根、实体,领域服务等一大堆其他概念。
      1. 聚合根,负责整个聚合业务的所有功能就行了。比如项目中的fileAggregate,该类直接负责与平台系统管理员相关的所有操作业务,对内依赖调用领域服务、其他实体,或封装一些不对外的方法、函数等,完成所有所需的功能,由聚合根对外统一提供方法。聚合根和其附属模型间有个共生死的约定(附属不可独自苟存)
      2. 实体有唯一的标识,有生命周期且具有延续性。例如一个交易订单,从创建订单我们会给他一个订单编号并且是唯一的这就是实体唯一标识。同时订单实体会从创建,支付,发货等过程最终走到终态这就是实体的生命周期。订单实体在这个过程中属性发生了变化,但订单还是那个订单,不会因为属性的变化而变化,这就是实体的延续性。
      3. 实体的代码形态:我们要保证实体代码形态与业务形态的一致性。那么实体的代码应该也有属性和行为,也就是我们说的充血模型,但实际情况下我们使用的是贫血模型。贫血模型缺点是业务逻辑分散,更像数据库模型,充血模型能够反映业务,但过重依赖数据库操作,而且复杂场景下需要编排领域服务,会导致事务过长,影响性能。所以我们使用充血模型,但行为里面只涉及业务逻辑的内存操作。
      4. 值对象:在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象。值对象没有唯一标识,没有生命周期,不可修改,当值对象发生改变时只能替换(例如String的实现)。值对象是描述实体的特征,作为实体的属性,数据库里一般是一个字段。
      5. 聚合。The reason for an aggregate is that the business logic will be applied on the aggregate, instead of each Entity holding the logic. An aggregate does not allow direct access to underlying entities( all fields in the aggregate struct begins with lower case letters). aggregates should only have one entity act as a root entity, this means that the root entity ID is the unique identifier of aggregate.
    2. 领域层不依赖基础层的实现,Entity 永远不能依赖非entity 包下的逻辑,在充血模型下,实际上 repository 是一部分 entity 的。Repository接口在领域层定义好,由infra层依赖领域层实现这个接口。数据库操作都是基于聚合根操作,保证聚合根里面的实体强一致性。PS: 一个domain一个repository接口
  7. 基础设施层。为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制等。
    1. 这一层也是讲究和业务逻辑无关,只重点提供通用的功能。主要需要编码的部分是仓储功能,单纯的(增删改查)数据库持久化等功能,变更的概率不大。
    2. 根据领域模型设计数据模型,而不是反过来。 一般原则,领域模型中的实体映射为数据库中的表;领域模型中的属性,映射成表中的字段,同时还要根据需求补充更多的字段。模型中的一个一对多关联,可以映射成一个外键字段,以及一个外键约束。但一般不会真的建立外键约束,而外键的逻辑关系还是存在的。可以用虚线箭头表示这种逻辑上的外键关系,称为虚拟外键。对于多对多关联,必须增加一个关联表,其中包括了两个实体表各自的主键。另外,关联上的多重性决定了外键字段的非空约束。

极客时间《DDD实战课》

  实体 值对象
业务形态 是多个属性、操作或行为的载体,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合 值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。
代码形态 实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。 值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。
运行形态 实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。 值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为就很少了。
数据库形态 与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。在领域模型映射到数据模型时,一般是一对一,也有一对多的情况。 数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。

为什么要在限界上下文和实体之间增加聚合和聚合根这两个概念?实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。举个例子。社会是由一个个的个体组成的,象征着我们每一个人。随着社会的发展,慢慢出现了社团、机构、部门等组织,我们开始从个人变成了组织的一员,大家可以协同一致的工作,朝着一个最大的目标前进,发挥出更大的力量。领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。传统数据模型中的每一个实体都是对等的,如果任由实体进行无控制地调用和数据修改,很可能会导致实体之间数据逻辑的不一致。而如果采用锁的方式则会增加软件的复杂度,也会降低系统的性能。如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

  1. 它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
  2. 它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  3. 在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

领域事件是解决子域之间耦合的重要手段,当一个领域事件发生时,它会触发一些操作,这些操作可能会更改系统的状态,也可能会导致其他领域事件的发生。

领域建模的体系化思维与6种方法论怎么判断模型好不好?当接到一个新的需求时,如果发现模型的关系不匹配了,那说明之前的模型设计的肯定存在问题,好的模型实体和实体之间的关系一定是稳定的,只会新增不会对原有关系进行改变。PS:变相支持了开闭原则。

领域模型为什么要一直持续迭代?

  1. 意识问题。在用户、产品人员、运营人员眼中,沟通的语音是”流程”而不是”模型”。开发人员在与他们的沟通过程中,慢慢就形成了以”流程”为主导,而不是以”模型”为主导的思维方式。这使得整个开发过程是”流程驱动”,而不是”领域驱动”。大家在讨论业务与系统解决方案的时候,大部分时间都花在了业务流程、业务规则上,而不是深刻挖掘流程背后的不变因素。
  2. 现实世界的复杂性。业务也就是我们的现实世界,灰度的、模棱两可的东西,比计算机的世界多得多,变化也多得多。很难确定有哪些东西是不怎么变的,什么东西是容易变的,而这恰恰是做建模的前提条件。
  3. 迭代速度。在稳固的模型,也不可能一成不变,毕竟现实世界一直在变。当现实世界变化到模型不能支撑的时候,要能马上修改模型才行。但实际情况是,因为开发效率的原因,工期赶不上,然后就会在旧的模型上进行打补丁,补丁一个接着一个打,最后整个系统臃肿不堪,开发效率进一步降低,如此恶性循环。
  4. 火候的掌握。领域模型是要对现实世界建模,既要去寻找不变性,又要为可能变化的地方留出扩展性。什么地方是不变的,要作为基础;什么地方是易变的,要留出扩展性,这其中并没有一个标准原则。另外,各家公司的业务规模、速度不一样,团队实施能力也不一样。所以在实践中,要么会”缺乏设计”,要么会”过度设计”。对火候的掌握,需要有悟性。只有反复思考,反复推翻自己之前的想法,再重建新的想法,才能在实践中不断找到领域模型、业务发展速度、技术团队能力之间的”最佳平衡点”。

CQRS

命令查询职责分离

  1. 命令(Command):不返回任何结果(void),但会改变对象的状态。实践时还是让命令返回了一些主键之类的。
    1. 命令抽象出来之后,可以串一下表现层/应用层对该命令的消费。
      1. 表现层:将表现层接受来的请求主体 转换为Command ,并且进行参数校验,触发Command执行。
      2. 应用层:命令的Handler,一般都有相关领域上下文的聚合根来承担。基本逻辑 ``` commandHanlder.Func{
      3. 实例化领域实体 // 查询我们需要处理的实体数据,然后创建对应的领域对象,构造我们所定义的聚合。
      4. 调用实体行为 // 调用行为会修改实体的属性是内存上的
      5. 保存实体变更 // 把变更保存到基础设施层,例如 MySQL,PG等 } ```
    2. 每一个确定的命令操作,不论成功还是失败,只要执行之后就产生相应的事件(Event)。
      1. 事件的Handler,某个命令处理完毕之后,也即某个事件发生了,有可能需要短信通知、邮件通知等等。事件订阅者继续后续的逻辑。
  2. 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。查询可以从应用层进行分离,直接操作infra层获取业务不是特别复杂的查询,这与没有引入CQRS的代码可以保持一致。PS: domain只是服务command,且domain 的持久化相对简单,一般都是根据id 进行crud。

ddd 缺点

为什么领域驱动实践了20年,效果并不好在领域驱动理论中,业务学习和领域建模的优先级最高是理所当然。

可惜,这两个阶段书中没有提供什么可供实践的操作方法论。

  1. 学习多少对领域建模是足够的?
  2. 学习哪些内容?什么学习方法是无效的?
  3. 领域模型一定存在么?
  4. 如果存在,如何构建?
  5. 如果构建完成,他具备什么特征?

这些都是问题。不解决根本问题,其他都是舍本追末。cqrs,eda,战略设计,战术设计……都是缘木求鱼。不单是空谈——这些特定场景的特定解决方案,在没有明确领域适用性的情况下——反而平台引入了不必要的技术复杂度。这不是领域驱动,这反而是违反领域驱动精神的。扛着红旗反红旗。

解决方案

  1. 如何快速学习一个陌生的领域,给出它的模型?咨询公司很多咨询师,从了解一个行业,都侃侃而谈把行业专家侃晕到五体投地,往往不过几个月。——这并不仅仅是因为他们掌握了某种话术。而是咨询公司形成了一套快速归纳一个行业业务模型的高效方法论。以流程为中心,关注客户,关注价值链。所以,和普通人学习业务更关注业务细节不同。他们学习业务,更关注业务流程背后的利益相关方,市场格局——等本质规律。然后,站在这样的高度,抓住了根本,反而可以忽略一切细节繁琐,直透本质。进行“流程再造”,提升业务效率,提升客户价值,提升整个业务流程的合理性,灵活性,通用性,标准型……对于软件架构师而言,掌握这套方法。就意味如同咨询师一样,并不需要学习漫无边际业务知识、和细节。而仅仅通过梳理一两个业务流程。就获得整体业务关键的、不变的相关方;通过3,4个业务流程,归纳出行业一般的,通用的步骤和能力……根据本质和一般再组合出任意需要的业务。
  2. 如何确保模型对领域是完备的?同归本质和归纳得出的模型。不是完备的。通过事件风暴,能够确保模型对当前和未来短期的需求是完备的。但是不能确保任意完备。所以DDD理论最后章节是柔性设计/重构。DDD方法论实际让认为“绝对的领域模型并不存在”而“只存在相对的领域模型”。
  3. 领域模型应该是什么样子的?架构的最大目标是控制复杂度。而在架构设计中存在两种不同的复杂度对象——必然复杂度和偶然复杂度。必然复杂度是这个问题领域存在客观规律。其复杂度是最小的,不可减少的。偶然复杂度是软件工程在解决这个问题中引入的额外复杂度,可能源自设计的不足,或者技术手段的不合理,组织结构的不合理,工程开发方法不合理…………导致付出的额外复杂度。偶然复杂度是会随着我们解决问题方法的不同而或有增减,但不可能为零。无论任何设计方案,任何技术手段,任何工程方法,都只会增加。而不会降低,可能是设计的不足,或者过度,或者设计本身就不符合问题的客观规律。技术选型错误,产生了额外的工程复杂度和学习复杂度,或者稳定性,性能等方面产生了太多问题……
    1. 让额外复杂度最低的方法就是在核心的设计阶段根本不考虑工程层面的问题。不考虑就不会有复杂度。偶然复杂度就为0。然后,再根据领域模型,结合业务场景(事件风暴),取舍出最必要的业务扩展,技术需求……这便是工程上理论上能达到的最小的复杂度。所以,在设计阶段,不必考虑任何技术实现层面的问题,分工协作层面的问题,工程落地方法上的问题。而仅仅是基于问题和需求本身,做分析和权衡。否则便是错误,让各种复杂度交织在一起,互相影响,一定是最差的决策。