山一程--架构设计--DDD-- 再读《IDDD》
目的:精化 领域驱动设计思想 , 再读 《实践领域驱动设计》
精要:
- 实施DDD时,设计就是代码,代码就是设计,白板图并不是设计,只是我们讨论模型的一种方式。
- 问题域空间由 战略核心域 及其 支撑子域 组成。
- Application layer 每一个方法都对应着一个单一的 用例流 ,不要用一个方法来处理多个用例流。 p20
- 限界上下文 和 通用语言 一对一
- 限界上下文是一个相对较小的概念,比我们当初想象的要小,刚好能够容纳下一个独立的业务领域所使用的通用语言.
- 上下文映射图集成多个限界上下文。
- 在单个限界上下文团队成员共享一套通用语言,不同的团队有时各自负责一个限界上下文,此时可以使用 上下文映射图 在战略层面对限界上下文进行界分和集成。在建模边界内部,使用战术建模工具:聚合,实体,值对象,领域服务,领域事件。
- 以数据为中心的方式, 客户断码必须知道如何正确地将一个待定想提交到冲刺中,这样的模型是不能称为领域模型的, 需要认证分析客户代码来完成映射,会暴露结构,将关注点集中在了数据属性上,而不是对象行为,行为要表明领域模型中的概念。
- 要将行为暴露给客户,行为方法的名字清楚地表明了业务含义。 p28
- DDD 在开发一个新的领域对象时,比如 实体 或 值 对象,采用以下步骤:
- 编写测试代码以模拟客户代码如何使用该领域对象
- 创建该领域代码以使测试通过
- 重构业务代码和测试代码,直到测试代码能够正确地模拟客户代码,同时领域对象拥有能够表明业务行为的方法签名。
- 实现领域对象的行为,直到测试通过为止,再对实现代码进行重构.
- 向团队成员,及领域专家展示代码,以保证领域对象能够正确地反映通用语言.
- 领域对象对于领域概念的表达,而测试代码本身便是通用语言在程序中的表达。
- 事件驱动,管道和过滤器,将一个大问题分解成若干个较小的步骤完成,使得分布式处理更容易理解和管理,
- 领域事件的名字反映业务操作。发出的事件表示某个限界上下文中某个聚合的行为输出。还可以创建和修改相应上下文中的聚合。
- 领域事件,并不是简单的事件通知,而是显式地对业务过程进行建模,对于整个领域范围内的订阅方来说是有好处的。还可以对这种同步式的,逐步式的处理方式进行扩展,以使其同时处理多个任务.
解决空间,开发核心域的解决方案是一种关键性业务投入
- 有哪些软件资产是已经存在的,它们可以重用吗?
- 哪些资产是需要创建的,或者从别处获得。还需要做什么
- 这些资产如何集成在一起的,还需要什么样的集成?
- 核心域和那些支撑项目的成功几率如何?会不会出现由于其中一个失败而导致整个项目失败的可能.
- 有哪些地方我们使用了完全不同的术语.
- 限界上下文之间在哪些地方存在概念重建.
- 这些重叠的概念在不同的限界上下文之间如何映射和翻译的。
- 哪些限界上下文包含了核心域的概念,哪些使用了战术模式。
识别概念分离正确的情况:
有些相似的对象拥有不同的属性和行为,此时我们可以认为上下文边界的划分是否合理。
如果在不同的限界上下文中看到了 完全相同的对象,通常意味着模型错误。除非有些限界上下文使用了 共享内核 (Shared Kernel)
2021-04-07
面向服务架构 SOA (Service - Oriented Architecture SOA) 原则
- 服务契约:通过契约文档,服务阐述自身得目的与功能。
- 松耦合:服务将依赖关系最小化。
- 服务抽象:服务只发布契约,而向客户隐藏内部逻辑。
- 服务重用性:一种服务可以被其他服务所重用。
- 服务自治性:服务自行控制环境与资源以保持独立性,有助于保持服务的一致性和可靠性.
- 服务无状态性:服务负责消费方的状态管理,这不能与服务的自治性发生冲突
- 服务可发现性:客户可以通过服务元数据来查找服务和理解服务。
- 服务组合型:一种服务可以由其他服务组合而成,而不管其他服务的大小和复杂性如何。
服务技术可以是 REST 资源,,SOAP 接口 或者消息类型。
不要用一个或多个技术服务断电 用于决定限界上下文的大小,结果将导致许多小的限界上下文和领域模型,这样的领域模型可能只包含
一个实体对象,且该实体作为某个单一聚合的根对象而存在。
每一个领域模型由一组SOA 服务开发服务所包围,这些服务是满足业务目标的.
REST
分布式系统架构存在着多种架构风格:
1. 客户端--服务器架构 风格
2. 分布式对象风格
2021-04-09
管道和过滤器消息模式
管道是消息通道 : 过滤器通过输入管道接收数据,通过输出管道发送数据, 实际上,管道即是一个消息通道.
端口连接过滤器和管道:过滤器通过端口连接到输入和输出管道。端口使得六边形架构称为首选的架构.
过滤器即是处理器:过滤器可以对消息处理,而不见得一定对消息过滤.
分离处理器:每个过滤处理器都是一个分离的组件。
松耦合:每个过滤处理器都相对独立参与处理过程,处理器组合可以通过配置完成.
可换性:根据用例需求,我们可以重新组织不同处理器的执行顺序,这同样是通过配置完成的。
过滤器可以使用多个管道:消息过滤器可以从不同的管道中读取数据,表示了一种并行的处理过程。
并行使用同种类型的过滤器:对于最繁忙的和最慢的过滤器来说,可以并行地采用多个相同类型的过滤器来增加处理量.
设计长时处理过程的不同方法
一般有三种方法:
1. 将处理过程设计成一个组合任务,使用一个执行组件对任务进行跟踪,并对各个步骤各任务完成情况进行持久化。
2. 将处理过程设计成一组聚合,这些聚合在一系列的活动中相互协作。一个或多个聚合实例充当执行组件,并维护整个处理过程的状态。
3. 设计一个无状态的处理过程,其中每一个消息处理组件都将对所接受到的消息进行扩充--即向其中加入额外的数据信息--然后再将消息发送到下一个组件。在这种方法中,整个处理过程的状态包含在每条消息中。
有并行处理, 有整个流程最后汇集到 Executive ,整个并行处理才算完成。结果合二为一。
有些订阅组件被部署在不同的计算节点上,此时的并行处理过程也是分布式的。

Executive 无法直到所接收到的两个领域事件是否来自同一个并行处理过程。处理过程并行启动,完成事件无序地产生。如何直到是
哪个处理过程执行完毕。
解决方案
1.在每个领域事件中加入处理过程的身份标识,这个标识可以和引发处理过程的领域事件的标识相同。可以使用UUID。
Executive 只有在接收到具有相同标识的领域事件时才会输出日志记录。然而,其并不会等待所有事件的到达,它也是一个事件订阅方,在事件到达时将自动启动相应的处理过程。

p138
长时处理过程的状态跟踪
消息的单次投递 Single delivery, 事件消重.
1. 执行器检查该到达事件的状态属性,表明该事件是否已经存在。如果状态已被设值,该事件便是一个重复事件。可忽略可应答。
2.将状态对象设计成幂等的。
二者都能达到消息传输的幂等性。
时间敏感性:
1. 被动的,在开始时计时,事件到达时判定。是否标记“遗弃”,缺点是,万一执行器始终接收不到完成领域事件。
2 .主动的,通过一个外部定时器来管理。缺点是:需要更多系统资源,定时器和完成事件的竞态条件可能造成系统失败。
长时处理过程 常和 分布式并行处理 联系在一起。与分布式事务没有什么关系,需要的是 最终一致性。
在基础设施或处理过程本身失败的时候,应该能够采取适当的修复措施。
只有在执行器收到整个处理过程成功的通知时,才能认为处理过程的各个参与方达到了最终一致性.
有些:整个处理过程并不需要所有的并行执行流都成功。
如果一个过程被搁浅,那么所有系统都处于一种不一致的状态,此时要做出一些补偿。虽然会增加处理过程的复杂性.
如果业务需求是允许失败情况发生的,此时采用工作流方案更合适。
当与遗留系统的集成储存在很大的时间延迟时,采用长时处理过程将非常有用。即便时间延迟和遗留系统并不是我们的关注点。
分布式和并行处理的优雅性,有助于我们开发高可伸缩性,高可用性的业务系统.
数据网织入 和基于网格的分布式计算
Data Fabric 一个好处是对领域模型提供了自然的支持,几乎消除了所有的阻抗失配。
聚合存储。聚合即是基于图的缓存中的 值部分,而聚合的唯一标识则是标识键。
这里的键即是聚合的唯一 标识 。
聚合的状态将被持久化为二进制数据或文本数据,这些便是值部分的内容.
String key = product.productId().id();
byte[] value = Serializer.serialize(product);
// region or cache
region.put(key, value)
Nosql 存储也可以i作为自然的聚合存储。用来简化DDD的技术实现.
数据复制
内存数据缓存, 一个具有多节点缓存的数据网织是可靠的。 从数据冗余性中获得更高的性能,受到节点数目的影响.
一次缓存,二级缓存 应对主缓存失效。可以保证对所发出事件的正确投递。
在聚合更新之前,数据网织所发出的事件是不会丢失的。
一个缓存层面的事件用于通知 诸如“重新初始化缓存”, 一个入口层面的事件描述诸如“创建入口和更新入口” 等操作.
领域事件, DomainEventPublisher 组件, Cached Event 通过同步或异步方式传达定于方。
2021-04-10
实体
开发要将关注点放在 富有行为的领域概念,而不是数据上. 使用DDD,将数据模型转变为实体模型.
1. 具有“唯一性”
2.生成唯一性的方法
3.从实体设计中捕获通用语言。
4.表达实体的角色和职责。
5.如何对实体进行验证和持久化。
2021-04-28
实体 可通过实现 不同的 角色接口,
也即 简单的表示某个角色,某个职责的接口。 在不同的上下文中履行不同的职责.
Entity 抽象类中 包含一个 vadalition() 抽象方法, 自此时开始可进行验证,
具体实体,要将其 getter 设置为 包级别可见,因为 validator 要跟其放在同一个包下。

2021-05-08
聚合:
不要在聚合中注入资源库 和 领域服务, 而在其他多数情况下,依赖注入都是很合适的,可以向应用服务注入资源库 和 领域服务。
2021-05-09
工厂方法能确保创建的实例处于正确的状态,完整的聚合,有效地表达限界上下文中的通用语言。
T extends Collaborator


Rest 接口调用, 替代繁琐的 Apache HttpClient : Jboss easyRest
book p361
https://blog.csdn.net/zhglance/article/details/54670216

2021-05-10
资源库
1. 面向集合的资源库, 模拟 集合, 无需 save, 从资源库中取出对象
2. 面向持久的资源库
面向集合的资源库, 具体流程: p368
1. 隐式读时复制 (Implicit Copy-on-Read) : 在从数据存储中读取一个对象时,持久化机制隐式地对该对象进行复制,再将该复制对象与客户端中的对象进行比较 。
2.隐式写时复制 Implicit Copy-on-Write : 持久化机制通过委派来管理所有被加载的持久化对象.
两种方式共同的优点:可以隐式地跟踪发生在持久化对象中的变化,而不需要客户端自行处理。
这里的底线是:持久化机制,例如 Hibernate, 能够允许我们创建一个传统的,面向集合的资源库。
流程:
1, 需要定义公有接口.
2, 至少需要提供一种实现.
面向集合的资源库:
1. 定义能够模拟集合的接口
2.然后使用持久化机制: Hinerbate.
业务角度上来将, 删除对象是不明智的。可以将聚合实例标记为 失活的, disabled, 不可用的 unusable,
或者从领域的角度对其进行逻辑删除。
不把那些删除方法放在资源库的公有接口中,或者可以在删除方法中只将聚合实例设置成不可用的。
ThreadLocal 来获取 session

Constraint Violation Exception, 违反约束


add, addAll 多次添加同一个CalendarEntry 情况, saveOrUpdate() 等任何显式更新都是没有效果的,
更新是随着 对象状态的变化而隐式完成。
除非所添加的对象是全新的,不然该 saveOrUpdate() 方法是没有任何效果。
注意: 一对一 映射时, 删除操作, 不能级联式地进行删除,必须同时显示地删除位于关联关系两端的对象:

1. ORM 所提供的生命周期事件来完成对象的级联删除。 却是 反对由 聚合来管理 持久化
只使用资源库来处理持久化。
DDD 不优先考虑使用聚合来管理持久化。
管理事务
讨论的事务 是关于 关系型数据的
应用层管理事务还有诸如 安全 方面的考虑。 为每一组相关用例创建一个门面 Facade
对事务的管理绝对不应该放在 领域模型 和 领域层中。通常来说,与领域模型相关的操作都是非常细粒度的,以致于无法用于管理事务。
领域模型也不应该意识到事务的存在。
要将事务 放在应用层中,然后为每个用例创建一个门面,门面中的业务方法通常都是粗粒度的,常见的情况是 每一个用例流 对应一个业务方法。
业务方法对用例所需操作进行协调。
当用户界面层调用门面中的一个业务方法时, 该方法都将开始一个事务。
同时,该业务方法将作为领域模型的客户端而存在。
在所有的操作都完成之后,门面中的业务方法将提交事务。在这个过程中,如果发生错误/异常,那么业务方法将对事务进行回滚.
可以通过 声明式的方法 或 自行编码 来管理事务。 对事务的管理过程都与以下执行过程相似:

要将对 领域模型的修改 添加到事务中, 必须保证资源库实现 与 事务使用了相同的 Session 或 Unit Of work. 这样在 领域层中发生的修改才能正确地提交到数据库中,或者回滚。

在任何一个持久化机制中,都需要在 整个业务 操作中访问相同的 Session, Unit of Work 和 由 应用层所管理的事务。
依赖注入是一个很好的选择。但是,在没有 依赖注入的情况下,依然可以找到很多方式来实施事务管理,
甚至可以通过手动的方式 来将这些对象绑定到当前线程中。
DAO 主要是从数据库表的角度来看待问题,且提供 CRUD 操作。
应尽量避免在领域模型中使用这些 DAO 模式,正常情况下,希望由聚合本身来管理业务逻辑.
2021-05-17
如果程序提供了 REST 资源,需要为领域模型创建状态展现以供客户端使用。
应该基于用例来创建状态展现,而不是基于聚合实例。
要将一组 REST 资源看作一个单独的模型 -- 视图模型 或 展现模型。
展现模型不应该与 领域模型中的 聚合状态 存在一一对应的关系。否则,你的客户端需要像聚合本身一样了解你的领域模型. 二者绑定,失去了抽象所带来的好处.
用例优化资源库查询
CQRS 查询
渲染适配器 以及处理用户编辑
需要在界面上显示 领域数据, 且允许用户编辑这些数据。
展现模型 渲染视图
1. 根据展现模型,视图完成自我渲染. 自然,在展现模型和视图之间完成了解耦
2. 视图由展现模型进行渲染。 测试虽然简单,但是将展现模型和 视图耦合起来。
展现模型可以适配 DTO, DPO, 或者用于发布聚合内部状态的调停者。
展现模型还可以跟踪用户的编辑,并不是向展现模型添加更多的职责,因为它本来就应该具有 双向的 适配功能,
从模型到视图,在从视图到模型.
展现模型并不是围绕这 应用服务 或者 领域模型的一个 重量级 门面。
应用服务
应用服务是 领域模型的 直接客户,
应用服务 负责 用例流的 任务协调,每个用例流 对应了 一个 服务方法。
在使用 ACID 数据库时,应用服务还负责 控制事务 以确保 对模型修改的原子提交。
通过消息集成 限界上下文
事件的到达可能是无序的,会导致异常的结果。因此,引入技术实现,和所使用的通用语言毫无关系。
但是,没有把 MemebrChangeTracker (team memeber 和 product owner ) 暴露到聚合边界之外。只是一个实现上的细节而已,
客户端无法感知到 M C T, 唯一需要提供的是 occurredOn 属性值, command 对象, 带上当前事件属性.

处理分布式系统之间的 合作者关系时 所采用的实现细节。
不同限界上下文之间 维护复制性信息来说,职责分离并非琐碎之事,需要考虑到 消息无序到达的情况 和 多次投递的情况.
REST 资源的方式在此时具有优势,因为接收通知的顺序 和 在事件存储忠 保存事件的顺序是一样的。 不同的消费方可以反复地通过
从头到尾的方式获取到事件通知.
唯一标识的共享 是安全且必须的。集成限界上下文的“最小化信息” 原则。
长时处理过程, 以及 避免职责
P450 事件 的方式 来达到 Forum, Discussion, Product 一致性 。
幂等操作。 消息的重新投递, 消息机制可能会多次发送同一条消息。
任何基础设施和架构所带来的影响, 幂等操作都将予以忽略,且无害。
对比 MemberChangeTracker 。
长时处理过程的状态机 和 超时跟踪器
TimeConstrainedProcessTracker 可重用的跟踪器概念 p450
监视那些在指定完成时间已经过期了的处理过程;
那对那些在过期之前可以任意重试的处理过程,也将进行监视。
该设计可以定期地对长时处理过程进行重试,
或者在不进行重试(或达到重试上限之后)的情况下 彻底超时。
属于技术子域,对跟踪器进行持久化或修改时,不用严格地遵循聚合原则.
跟踪器也是 相对独立的 ,且与长时处理过程存在这一对一的关系。因此,并发冲突的可能性并不大。
如果的确发生了并发冲突,我们依然可以依赖于消息重发来解决问题,在消息投递过程中产生的任何异常都会
导致监听器做出否定应答,进而使得RabbitMQ 重发消息。
需要多步完成的情况,
2021-05-29
《DDD 领域驱动设计精粹》 聚合部分
聚合设计的步骤
1. 重点放在聚合设计的规则2 :“聚合要设计得小巧”。 每个聚合一开始创建时只允许包含一个实体,且将它作为聚合根。不要尝试在边界内放入两个实体。
用你认为和 单个聚合根关联最紧密得 字段/属性 填充每个实体.
定义出每个用来 识别 和 查找 聚合的 字段/ 属性, 以及任何其他用来 构造 聚合并使之 处于有效 初始状态的 内在属性。
2. "聚合边界内保护 业务不变性规则“ :至少在单个 实体持久化时 所有内在 字段/属性 必须时最新的。
现在要查看每个聚合, 是否存在有关联性的更新, 记录所有基于相应的更新的时间范围。
3. 询问 领域专家, 每个基于响应的更新可以等待多长时间。
4. 对于 3中 即时更新的, 将聚合 A1 和 聚合 A2 合并成一个聚合
5. 对于 3 中有给定时间的 例如 30秒, 可遵循 ” 利用最终一致性更新其他聚合“, 通过领域事件 和 消息机制
限界上下文内的,采用轻量级的消息机制,
限界上下文 之间的, 采用 消息传递系统.

浙公网安备 33010602011771号