技术

Spark 内存管理及调优 Yarn学习 Spark部署模式及源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器 学习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内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 深度学习泛谈 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快速入门

架构

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

标签


领域驱动理念入门

2017年12月25日

简介

一文教会你如何写复杂业务代码一般来说实践 DDD 有两个过程:

  1. 套概念阶段。了解了一些 DDD 的概念,然后在代码中“使用”Aggregation Root,Bounded Context,Repository 等等这些概念。更进一步,也会使用一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。
  2. 融会贯通阶段。术语已经不再重要,理解 DDD 的本质是统一语言、边界划分和面向对象分析的方法。

作者认为自己处于1.7阶段。在现实业务中,很多的功能都是用例特有的(Use case specific)的,如果“盲目”的使用 Domain 收拢业务并不见得能带来多大的益处。相反,这种收拢会导致 Domain 层的膨胀过厚,不够纯粹,反而会影响复用性和表达能力。我们承认模型不是一次性设计出来的,而是迭代演化出来的。不强求一次就能设计出 Domain 的能力,也不需要强制要求把所有的业务功能都放到 Domain 层,而是采用实用主义的态度,即只对那些需要在多个场景中需要被复用的能力进行抽象下沉,而不需要复用的,就暂时放在 App 层的 Use Case 里就好了。PS:Use Case 是《架构整洁之道》里面的术语,简单理解就是响应一个 Request 的处理过程。

DDD被高估了吗?提出并识别模式,给它们起个名字,并使用它们来给出系统结构,保证系统统一性。我们可以并且应该发明自己的语言,并将任何 DDD 材料视为起点,而不是最终结果。如果你所做的一切都是按照现有的 DDD 标准术语定义,并试图将任何问题都硬塞到现有的结构中,那么你的生活将非常悲惨。有一种生活超越了 DDD,尽管我认可它应该总是由领域驱动的,只是不一定是 DDD 意义上的。

如何设计一个复杂的业务系统? 未读

革新的对象——面向数据库的架构/传统分层架构

领域驱动设计(DDD:Domain-Driven Design)提到服务器后端发展三个阶段

  1. UI+DataBase的两层架构,这种面向数据库的架构没有灵活性。
  2. UI+Service+DataBase的多层SOA架构,这种服务+表模型的架构易使服务变得囊肿,难于维护拓展,伸缩性能差
  3. DDD+SOA的事件驱动的CQRS读写分离架构,应付复杂业务逻辑,以聚合模型替代数据表模型,以并发的事件驱动替代串联的消息驱动。真正实现以业务实体为核心的灵活拓展。

领域驱动设计和开发实战不投入资源去建立和开发领域模型,会导致应用架构出现“肥服务层”和“贫血的领域模型”,在这样的架构中

  1. 外观类(通常是无状态会话 Bean)开始积聚越 来越多的业务逻辑,而领域对象则成为只有 getter 和 setter 方法的数据载体。
  2. 这种做法还会导致领域特定业务逻辑和规则散布于多个的外观类中(有些 情况下还会出现重复的逻辑)。
  3. 在大多数情况下,贫血的领域模型没有成本效益;它们不会给公司带来超越其它公司的竞争优势,因为在这种架构里要实现业务需求变更,开发并部署到生产环境中去要花费太长的时间。

基于数据库设计

多视角理解领域驱动

分解复杂性视角

领域驱动设计在互联网业务开发中的实践 解决复杂和大规模软件的武器可以被粗略地归为三类:抽象、分治和知识。

  1. 分治 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。
  2. 抽象 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐他们需要注意什么。PS:《原则》中也有类似的表述,你在思考高层次的事情时,一定不要考虑低层次的细节。《重构》中讲一个方法只要 包含跟该方法同级层次的代码。 谈谈业务开发中的抽象思维 抽象思维的三个阶段
    1. 经验归纳,只要我们认真做好手头的工作,就事论事地保持积累和总结,把这些经验进行系统化的记录、归纳、整理、分类,对于较复杂的、专业性较强的业务领域,甚至可以著书立说。
    2. 建模,有一点哲学认识论的味道,涉及到人类知识如何对客观世界进行刻画的问题。类比到物理学领域,物理定律对于客观的物理规律的描绘,实际上也是一种「建模」。经验归纳的思维方法,主要是对信息进行收集,以及简单的加工整理;而建模方法考验的主要是逻辑思维过程,对于信息的了解,所占比重已大幅下降。建模可以看作是对信息的深度加工整理。
    3. 高层抽象,主要是为了应对问题规模,把握更「大」的东西。首先,并不是所有人都会遇到规模足够大的问题需要解决。只有对于规模庞大的公司、组织、政府机构,这种思维方式才是必不可少的。更进一步,高层抽象需要处理模型和模型之间,甚至是体系和体系之间的关系问题。还是类比到物理学领域,如果把牛顿运动定律、相对论和量子力学看作三个不同的模型,那么高层抽象就相当于要描述清楚这三个理论体系之间的关系。类似这种「大一统」的思维方式,自然是抽象层次最高,也最难的。
  3. 知识 顾名思义,DDD可以认为是知识的一种。DDD提供了这样的知识手段,让我们知道如何抽象出限界上下文以及如何去分治。

西瓜可以横着切也可以纵着切,分治怎么分也要找到一个切口。

在系统复杂之后,我们都需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。技术维度是类似MVC这样,业务维度则是指按业务领域来划分系统。微服务架构更强调从业务维度去做分治来应对系统复杂度,而DDD也是同样的着重业务视角。DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构

领域驱动设计学习输出「DDD」则把大多数的业务逻辑都包含在了「聚合」、「实体」、「值对象」里面,简单理解也就是实现了对象自治,把之前暴露出来的一些业务操作隐藏进了「域」之中。每个不同的区域之间只能通过对外暴露的统一的聚合根来访问,这样就做了收权的操作,这样数据的定义和更改的地方就聚集在了一处,很好的解决了复杂度的问题。

软件架构设计视角

Evic Evans在《领域驱动设计》中将软件系统的设计分为2个部分:战略设计和战术设计。

  1. 战略设计部分指导我们如何拆分一个复杂的系统。 PS: 和微服务的划分不谋而合
  2. 战术部分指导我们对于拆分出来的单个子系统如何进行落地,在落地过程中应该遵循哪些原则。PS:对应大部分技术开发同学的工作。

为什么是“领域”驱动,而不是什么别的东西驱动?比如服务驱动?对象驱动?

  1. 简单的系统数据库CRUD就可以搞定,只有足够复杂且多变(二者缺一不可)的系统才用得上领域驱动
  2. 开发人员经常把业务流程实现成系统流程,业务流程复杂、多变的时候, 系统流程也必须做出改变,因而需要在“业务流程”和“系统流程”之间提出一层,即领域模型
  3. 领域就是现实世界的业务,是复杂多变的,我们看到的只是现象。而领域模型就是要找到这些现象背后不变的部分,也就是本质,也就是“变化”背后的“不变性”
  4. 就像任何一门语言,最基本的是单词。领域驱动设计的一系列概念:实体、值对象、聚合根、领域事件、Specification,就是领域模型这门“建模”语言的“单词”。给了我们一系列分析工具,帮我们分析出“领域”现象背后的“本质”。

所以,换句话说,本质是业务流程和系统流程不一致带来许多问题, 需要抽一个中间层,这个中间靠近业务/领域,所以以“领域”方式描述,但又不能易变,所以必须找到业务中不变的部分(即本质),来减少系统流程的变动

如何用代码有效描述业务视角

领域驱动设计学习输出DDD 改变了传统软件开发工程师针对数据库进行的建模方法,从而将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为通过合理运用面向对象的封装、继承、多态等设计要素,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。

阿里盒马领域驱动设计实践

  1. 传统项目中,架构师交给开发的一般是一本厚厚的概要设计文档,里面除了密密麻麻的文字就是分好了域的数据库表设计。言下之意:数据库设计是根本,一切开发围绕着这本数据字典展开
  2. 我经常会做一个假设:假设你的机器内存无限大,永远不宕机,在这个前提下,我们是不需要持久化数据的,也就是我们可以不需要数据库,那么你将会怎么设计你的软件?这就是我们说的 Persistence Ignorance:持久化无关设计。首先一点,领域模型无法通过数据库表来表示了,就要基于程序本身来设计了。
  3. 按照 object domain 的思路,领域模型存在于内存对象里,意味着得 通过 类图 而不是ER图来描述业务。用类 比用 数据库表 有更丰富的表达方式:通过引用来表达多对多关系、封装、继承和多态等。

领域驱动设计和开发实战从设计和实现的角度来看,典型的 DDD 框架应该支持以下特征:应该是一个以 POJO(这里说的应该是充血对象)为基础的架构;领域第一,基础设施第二:PersistentObject只是表达了一种存储方式而已, 跟业务毫无关系。

领域模型设计

主要来自极客时间《软件设计之美》

  1. 战术设计(Tactic DDD):Entity, Value Object; Aggregate, Root Entity, Service, Domain Event; Factory, Repository。
  2. 战略设计(Strategic DDD):Bounded Context, Context Map; Published Language, Shared Kernel, Open Host Service, Customer-Supplier, Conformist, Anti Corruption Layer (context relationship types)。

战略设计:子域、限界上下文和上下文映射图

  1. 软件开发是解决问题,而解决问题要分而治之。所谓分而治之,就是要把问题分解了,对应到领域驱动设计中,就是要把一个大领域分解成若干的小领域,而这个分解出来的小领域就是子域(Subdomain)
  2. 对于一个真实项目而言,划分出来的子域可能会有很多,但并非每个子域都一样重要。所以,我们还要把划分出来的子域再做一下区分,分成核心域(Core Domain)、支撑域(Supporting Subdomain)和通用域(Generic Subdomain)。核心域是整个系统最重要的部分,是整个业务得以成功的关键。关于核心域,Eric Evans 曾提出过几个问题,帮我们识别核心域:为什么这个系统值得写?为什么不直接买一个?为什么不外包?
  3. 有了切分出来的子域,怎样去落实到代码上呢?首先要解决的就是这些子域如何组织的问题,是写一个程序把所有子域都放在里面呢,还是每个子域做一个独立的应用,抑或是有一些在一起,有一些分开。这就引出了领域驱动设计中的一个重要的概念,限界上下文(Bounded Context)。它形成了一个边界,一个限定了通用语言自由使用的边界,一旦出界,含义便无法保证。比如,同样是说“订单”,一旦定义了限界上下文,那交易上下文的“订单”和物流上下文的“订单”肯定是不同的。
  4. 很自然地,我们就可以把限界上下文看作是一个独立的系统,比如,每个限界上下文都可以成为一个独立的服务。限界上下文的重点在于,它是完全独立的,不会为了完成一个业务需求要跑到其他服务中去做很多事,而这恰恰是很多微服务出问题的点,比如,一个业务功能要调用很多其他系统的功能。
  5. 有了对限界上下文的理解,我们就可以把整个业务分解到不同的限界上下文中,但是,尽管我们拆分了系统,它们终究还是一个系统,免不了彼此之间要有交互。所以,我们就要有一种描述方式,将不同限界上下文之间交互的方式描述出来,这就是上下文映射图(Context Map),DDD 给我们提供了一些描述这种交互的方式,比如:合作关系(Partnership);共享内核(Shared Kernel);客户 - 供应商(Customer-Supplier);跟随者(Conformist);防腐层(Anticorruption Layer);开放主机服务(Open Host Service);发布语言(Published Language);各行其道(Separate Ways);大泥球(Big Ball of Mud)。
  6. 当我们知道了不同的限界上下文之间采用哪种交互方式之后,不同的交互方式就可以落地为不同的协议。比如REST API、RPC 或是MQ

战术设计

战术设计包含了很多概念,比如,实体、值对象、聚合、领域服务、应用服务等等。有这么多概念,我们该如何区分和理解他们呢?我们同样需要一根主线。

  1. 首要任务就是设计角色,在战术设计中,我们的角色就是各种名词。识别名词也是很多人对于面向对象的直觉反应。有一些设计方法会先建立数据库表,这种做法本质上也是从识别名词入手的。我们在战术设计中,要识别的名词包括了实体和值对象。什么是实体呢?实体(Entity)指的是能够通过唯一标识符标识出来的对象。在业务处理中,有一类对象会有一定的生命周期。以电商平台上的订单为例,它会在一次交易的过程中存在,而在它的生命周期中,它的一些属性可能会有变化,比如说,订单的状态刚开始是下单完成,然后在支付之后,变成了已支付,在发货之后就变成了已发货。但是这个订单始终都是这个订单,因为这个订单有唯一的标识符,也就是订单号
  2. 还有一类对象称为值对象,它就表示一个值。比如,订单地址,它是由省、市、区和具体住址组成。它同实体的差别在于,它没有标识符。实体的属性是可以变的,只要标识符不变,它就还是那个实体。但是,值对象的属性却不能变,一旦变了,它就不再是那个对象,所以,我们会把值对象设置成一个不变的对象。我们为什么要将对象分为实体和值对象?其实主要是为了分出值对象,也就是把变的对象和不变的对象区分开。一方面,我们会把一些值对象当作实体,但其实这种对象并不需要一个标识符;另一方面,也是更重要的,就是很多值对象我们并没有识别出来,比如,很多人会用一个字符串表示电话号码,会用一个 double 类型表示价格,而这些东西其实都应该是一个值对象。之所以说这里缺少了对象,原因就在于,这里用基本类型是没有行为的。在 DDD 的对象设计中,对象应该是有行为的。比如,价格其实要有精度的限制,计算时要有自己的计算规则。如果不用一个类将它封装起来,这种行为就将散落在代码的各处,变得难以维护。只有数据的对象是封装没做好的结果,一个好的封装应该是基于行为的。在 DDD 的相关讨论中,经常有人批评所谓的“贫血模型”,说的其实就是这种没有行为的对象。
  3. 选定了角色之后,接下来,我们就该考虑它们的关系了。
  4. 在传统的开发中,我们经常会遇到一个难题。比如,如果有一个订单,它有自己对应的订单项。问题来了,取订单的时候,该不该把订单项一起取出来呢?取吧,怕一次取出来东西太多;不取吧?要是我用到了,再去一条一条地取,太浪费时间了。这就是典型的一对多问题,也是一种用技术解决业务问题的典型思路。我们之所以这么纠结,主要就是因为我们考虑问题的出发点是技术,如果我们把考虑问题的出发点放到业务上呢?战术设计就给了我们这样一个思考的维度:聚合。聚合(Aggregate)就是多个实体或值对象的组合,这些对象是什么关系呢?你可以理解为它们要同生共死。比如,一个订单里有很多个订单项,如果这个订单作废了,这些订单项也就没用了。所以,我们基本上可以把订单和订单项看成一个单元,订单和订单项就是一个聚合。
  5. 一个聚合里可以包含很多个对象,每个对象里还可以继续包含其它的对象,就像一棵大树一层层展开。但重点是,这是一棵树,所以,它只能有一个树根,这个根就是聚合根。聚合根(Aggregate Root),就是从外部访问这个聚合的起点。其实,我们可以把所有的对象都看成是一种聚合。只不过,有一些聚合根下还有其他的对象,有一些没有而已。这样一来,你就有了一个统一的视角看待所有的对象了。那如果不同的聚合之间有关系怎么办?比如,我要在订单项里知道到底买了哪个产品,这个时候,我在订单项里保存的不是完整的产品信息,而是产品 ID。还记得吗?实体是有唯一标识符的。有了对于聚合的理解,做设计的时候,我们就要识别出哪些对象可以组成聚合。一对多问题也就不再是问题了:是聚合的,我们可以一次都拿出来;不是聚合的,我们就靠标识符按需提取
  6. 有角色了,也确定关系了。接下来,就要安排互动了
  7. 事件风暴 识别出了事件和动作,而故事的来龙去脉其实就是这些事件和动作。因为有了各种动作,各种角色才能够生动地活跃起来,整个故事才得以展开。动作的结果会产生出各种事件,也就是领域事件,领域事件相当于记录了业务过程中最重要的事情。那各种动作又是什么呢?在战术设计中,领域服务(Domain Service)就是动词。只不过,它操作的目标是领域对象,更准确地说,它操作的是聚合根。动词,是我们在学习面向对象中最为缺少的一个环节,很多教材都会教你如何识别名词。在实际编码中,我们会大量地使用像 Handler、Service 之类的名字,它们其实就是动词。PS:面向对象单纯描述对象和对象之间的关系, 还是不能说清楚全貌啊
  8. 动作不应该在实体或值对象上吗?确实是这样的,能放到这些对象上的动作固然可以,但是,总会有一些动作不适合放在这些对象上面,比如,要在两个账户之间转账,这个操作牵扯到两个账户,肯定不能放到一个实体类中。这样的动作就可以放到领域服务中。还有一类动作也比较特殊,就是创建对象的动作。显然,这个时候还没有对象,所以,这一类的动作也要放在领域服务上。这种动作对应的就是工厂(Factory)(其实就是设计模式中常提到的工厂)。工厂创建聚合根,聚合根创建聚合里的各种子对象(以便保证二者之间的关联)。
  9. 对于这些领域对象,无论是创建,还是修改,我们都需要有一个地方把变更的结果保存下来,而承担这个职责的就是仓库(Repository)。你可以简单地把它理解成持久化操作(当然,在不同的项目中,具体的处理还是会有些差别的)。
  10. 当我们把领域服务构建起来之后,核心的业务逻辑基本就成型了。但要做一个系统,肯定还会有一些杂七杂八的东西,比如,用户要修改一个订单,但首先要保证这个订单是他的。在 DDD 中,承载这些内容的就是应用服务。应用服务和领域服务之间最大的区别就在于,领域服务包含业务逻辑,而应用服务不包含。一些与业务逻辑无关的内容都会放到应用服务中完成,比如,监控、身份认证等等。

深入理解领域驱动设计中的聚合 聚合的本质就是建立了一个比对象粒度更大的边界,聚集那些紧密关联的对象,形成了一个业务上的对象整体。使用聚合根作为对外的交互入口,从而保证了多个互相关联的对象的一致性。通过把对象组织为聚合,在基本的对象层次之上构造了一层新的封装。封装简化了概念,隐藏了细节,在外部需要关心的模型元素数量进一步减少,复杂性下降。但是不是所有相关对象都聚合到一块呢?聚合划分的原则

  1. 生命周期一致性。生命周期一致性是指聚合边界内的对象,和聚合根之间存在“人身依附”关系。即:如果聚合根消失,聚合内的其他元素都应该同时消失。
  2. 问题域一致性。个图书网站,用户可以对图书发表评论。如果只是因为文章删除和评论删除之间存在逻辑上的关联,就让文章聚合持有评论对象,那么显然就约束了评论的适用范围。一目了然的事实是,评论这一个概念,在本质上和文章这个概念相去甚远。
  3. 场景频率一致性
  4. 聚合内的元素尽可能少

读写分离

DDD读写对待不一样的,写需要严格遵守分层结构。读不一定,看情况。

领域模型是用于领域操作的,当然也可以用于查询(read),不过这个查询是有代价的。在这个前提下,一个 aggregate 可能内含了若干数据,这些数据除了类似于 getById 这种方式,不适用多样化查询(query),领域驱动设计也不是为多样化查询设计的。 查询是基于数据库的(比如 获取某数据的列表,这是一个查询需求,不算业务模型之内。业务模型一般侧重于 几个抽象 以及 抽象之间的相互作用),所有的复杂变态查询其实都应该绕过 Domain 层,直接与数据库打交道。

横着看

细节:贫血模型和充血模型

我们必须将应用程序的业务逻辑从服务层迁移到领域模型类中,为何呢? 先来看看贫血模型和充血模型的对比。

举个具体的例子,假设一个用户有很多收货地址

class User{
    List<Address> addresses;
    setter
    getter
}

那么在为用户添加收货地址时,不得不有很多判空操作

class UserService{
    void addAddress(User user,Address address){
        List<Address> addresses = user.getAddresses();
        if(null == addresses){
            addresses = new ArrayList<Address>();
            user.setAddresses(addresses);
        }
        addresses.add(address);
    }
}

想象一下

  1. 如果有多个位置操作User的Address(这个例子针对这一点不是很适当),if(null == addresses){...} 会大量出现,代码量不大, 但会很丑。如果是电商业务,每一次购物都要做优惠券、红包、满减检查、余额不足检查等,这些逻辑有可能重复在各个Service中。
  2. 更复杂的成员变量 List<List> 或者 List<Map<String,String>>
  3. 更复杂的逻辑,比如设定默认地址,地址判重等。

UserService.addAddress吐血表示,我只想添加个地址而已。

换成充血模型

class User{
	List<Address> addresses;
	public User(){
		addresses = new ArrayList<Address>();
	}
	void addAddress(Address address){
		addresses.addAddress(address)
	}
}
class UserService{
	void addAddress(User user,Address address){
		...
		user.addAddress(address);	
		...
	}
}

从中可以看到,addresses的 初始化和 添加都由User 负责,代码简洁很多。

PersistentObject一般由框架自动生成,不适合做改动,只提供setter/getter方法,或者说除了set/get什么都做不了。这样不得不很多逻辑放在XXService中,造成XXService的臃肿。直接暴露set/get很多时候是有不安全的。

如何从容应对复杂性基于贫血模型的传统开发模式,将数据与业务逻辑分离,违反了 OOP 的封装特性,实际上是一种面向过程的编程风格。充血模型是一种有行为的模型,模型中状态的改变只能通过模型上的行为来触发,同时所有的约束及业务逻辑都收敛在模型上。User的行为交由自己去管理, 而不是交给各种Service去管理。面向对象设计主张将数据和行为绑定在一起也就是充血模型,而贫血领域模型则更像是一种面向过程设计,贫血领域模型的根本问题是,它引入了领域模型设计的所有成本,却没有带来任何好处。最主要的成本是将对象映射到数据库中,从而产生了一个O/R(对象关系)映射层。只有当你充分使用了面向对象设计来组织复杂的业务逻辑后,这一成本才能够被抵消

DDD不只是指导写代码

领域驱动设计学习输出面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰的问题域。通过对问题域进行分析和建模,识别限界上下文,利用它划分相对独立的领域,再通过上下文映射建立它们之间的关系,辅以分层架构与六边形架构划分系统的逻辑边界与物理边界,界定领域与技术之间的界限。之后,进入战术设计阶段,深入到限界上下文内对领域进行建模,并以领域模型指导程序设计与编码实现。若在实现过程中,发现领域模型存在重复、错位或缺失时,再进而对已有模型进行重构,甚至重新划分限界上下文。

小结

2018.6.20 补充 大家一直在谈的领域驱动设计(DDD),我们在互联网业务系统是这么实践的 本文字字珠玑,适合细读。

你写的代码是别人的噩梦吗?从领域建模的必要性谈起

领域驱动设计在互联网业务开发中的实践

DDD领域驱动设计基本理论知识总结

领域驱动设计示例