书山天道_iDDD 读书笔记
记录《实现领域驱动设计》知识
《实现领域驱动设计》
感悟1.
-
测试驱动很有用,能在其中发现和完善对象内容。 P172
- 通过模拟客户端对值对象的使用,这些测试可以驱动出对领域模型的设计。
- 根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。无论使用什么技术来完成数据建模,数据库实体,主键,引用完整性和索引都不能用来驱动你对领域概念的建模。DDD 不是关于如何根据范式来组织数据的,而是在一个一致的限界上下文中建模一套通用语言。
- 虽然在对象和关系数据库之间存在阻抗失衡,依然由合适的映射方式。Hibernate映射和数据库表定义,提供了优化的,可查询的持久化对象。
- 从领域模型中发布领域事件.一种简单高效的发布领域事件的方法便是使用观察者Observer模式,这种方法可以在领域模型和外部组件之间进行解耦。领域模型绝不能和消息中间件显示耦合。
- 聚合的一大原则:在一个事务中,只对一个聚合进行修改。所有聚合实例之间的最终一致性必须通过异步的方式予以处理。
- Web的好处--通过内奸的缓存机制可以增强系统的性能和可伸缩性。
记录
- 2021-01-10 22:16:55 第一遍通读完成。
1. 重要知识点
chapter 5
1.实体本质
- 考虑一个对象的个性特征,区分不同的对象时,引入实体Entity这个领域概念。实体是一个唯一的东西,可以在相当长的事件内持续地变化,可对实体做多次修改,由于它们拥有相同的身份标识,依然是同一个实体。
- 要慎重对待实体,什么时候发生了改变,发生了什么改变,谁做出了改变。唯一的身份标识和可变性 mutability特征将实体对象和值对象区分开来。
- 在实体设计早期,要可以把关注点放在能体现实体身份唯一性的主要属性和行为上,同时将关注如何对实体进行查询,可以忽略次要的属性和行为。
- 在设计实体时,首先考虑实体的本质特征,特别是实体的唯一标识和对实体的查找,而不是一开始便关注实体的属性和行为,只有在对实体的本质特征有用的情况下,才加入相应的属性和行为。
- Entity id 由 Value Object ( 不可变 ) 来维护相关的信息。避免扩散逻辑。
- 程序生成的标识,什么样的对象可以作为创建标识的工厂对象?对于聚合根的唯一标识,可以采用资源库来生成唯一标识。HibernateProductRepository.
2.实体的关键行为
- 一个领域事件应该对应于逻辑上的单个命令, 只有在符合通用语言的情况下才能使用setter方法,也或者,只有当我们不必使用多个setter方法来完成单个请求时,才有道理使用setter方法,多个setter方法使意图充满了歧义。
- Intention Revealing Interface 意图展现接口。
- 在成功执行所有的修改行为之后都需要向外发布领域事件,事件的必要性。事件至少可以完成两项功能。
- 有了事件,我们可以对对象的整个生命周期进行跟踪
- 事件可以通知外界订阅方完成同步操作,从而使这些订阅方具有潜在的自治性。
3. 角色和职责
- 实体 Customer 实现了两个细粒度接口: 1. AddOrdersToCustomer addOrder(anOrder:Order) 将订单添加给客户 2.MakeCustomerPreserred makePreferred() 将客户变成优先客户。 使用名词和形容词来命名接口,这种风格的好处:实体的角色可以在不同的用例之间发生转变。
- 两种情况下的Customer所扮演的角色使不同的.
- 技术上的好处,不同的用例所使用的 Customer获取策略可能是不同的
- 通过使用泛型,持久化机制从基础设施中查找不同的获取策略。
- 好的接口设计也有助于实现类。将功能实现在其自身上,而没有必要将实现委派给其他类。
- 角色接口最实用之处,最简单之处。通过接口,将细节隐藏起来。所设计的接口应该刚好满足客户端需求。业务领域的模型是最重要的。
4. 创建实体
- 新建一个实体,通过构造函数来初始化足够多的实体状态:
- 一方面有助于表明该实体的身份。
- 另一方面帮助客户端更容易地查找该实体。在使用及早生成唯一标识策略时,构造函数至少需要接收一个唯一标识符。如果还有可能通过其他方式对实体进行查找,例如名字或描述信息,应该将这些参数也一并传给构造函数。
- 如果一个实体维护了一个或多个不变条件invariant,不变条件即是在整个实体生命周期中都必须保持事务一致性的一种状态。若实体的不变条件要求该实体所包含的对象都不能为null,或者由其他状态计算所得,这些状态需要作为参数传递给构造函数。
5.验证
- 子封装性要求无论以哪种方式访问数据,即使从对象内部访问数据,都必须通过getter和setters.好处:
- 为对象的实例变量和类变量提供了一层抽象
- 可以方便地在对象中访问其所引用对象的属性。
- 自封装性使验证变得简单。
- Mysql row长度限制为 65,535字节,如果一个列定义为 65,535的 VARCHAR类型,其他列便没有存放空间了。
- 验证整体对象, 可以创建一个单独的组件来完成模型验证。设计单独的验证类时,可以将该类放在和实体相同的模块和(包)中,访问属性。验证类可以实现 Specification 或者策略模式。当发现非法状态时,验证类将通知客户方或者记录下验证结果以备后用,验证过程收集所有的验证结果,而不是在一开始遇到非法状态就抛出异常。
- 对复杂对象进行验证时,不是单独的实体是否合法,而是多个实体的组合是否全部合法,包括一个或多个聚合实例。一种方法是需要创建继承自Validator的不同验证类实例。但最好的方式是把这样的验证过程创建成一个领域服务。该领域服务可以通过资源库读取那些需要验证的聚合实例,然后对每个实例进行验证,可以是单独验证,也可以和其他聚合实例组合起来验证。发送领域事件的方式通知。
6.跟踪变化
跟踪变化最实用的方法是领域事件和事件存储。为领域专家所关心的所有状态改变都创建单独的事件类型,事件的名字和属性表明发生了什么样的事件。当命令操作执行完后,系统发出这些领域事件。事件的订阅方可以接收发生在模型上的所有事件。在接收到事件后,订阅方将事件保存在事件存储中。
事件源模式
Chapter 6 值对象
原则:尽量使用值对象来建模而不是实体对象,即使一个领域概念必须建模成实体,在设计时应该更偏向于将其作为值对象容器,而不是子实体容器。
在将领域概念建模成值对象之前,应该将通用语言考虑在内,这是建模值对象的首要原则。
当你只关心某个对象的属性时,该对象便可作为一个值对象,为其添加有意义的属性,并赋予它相应的行为。看成不变对象,不要给它任何身份标识,避免实体一样的复杂性。
值对象的特征 :
- 它度量或者描述了领域中的一件东西。
- 可以作为不变量。
- 将不同的相关的属性组合成一个概念整体 Conceptual Whole。
- 当度量和描述改变时,可以用另一个值对象予以替换。
- 可以和其他值对象进行相等性比较。
- 不会对协作对象造成副作用。
不变性:
- 在构造函数内便确定值对象最终状态,不开放可变接口
- 值对象中的实体对象引用,如果可能违背值对象的不变性,便没有理由引用该实体对象。
概念整体:
- 在一个领域中,概念的整体性是非常重要的。 p198
- 每个值对象都是一个内聚的概念整体,表达了通用语言中的一个概念。
- 值对象的构造函数用于保证概念整体的有效性和不变性。希望值对象的构造函数可以一次性地构建好整个值对象。
值对象相等性:
- equals() 方法设定是对两个值对象的属性进行判定。
- hashCode()
无副作用行为:
- 一个对象的方法可以设计成一个无副作用函数 Side-Effect-Free Function, 这里的函数表示对某个对象的操作,只用于产生输出,而不会修改对象的状态。
- 由于在函数执行的过程中没有状态改变,这样的函数操作也称为无副作用函数。
- 对于不可变值对象而言,所有的方法都必须是无副作用函数,不能破环值对象的不可变性。
- 值对象尽量只依赖于它自己的属性,只理解它自身的状态。
- 将实体作为参数的值对象方法中,很难看出该方法是否会对实体进行修改,测试也很困难。传给值对象方法的参数依然应该是值对象。可以获得更高层次的无副作用行为。增加一个值对象的健壮性。
- 将领域特定的无副作用函数分配给 特定的值对象(Value Object ),而非语言提供的基本值对象primitive : int float ... ,否则无法在深层次上捕获领域概念。
- 有些真正简单的属性,布尔类型或数值类型的值对象,可能能够自给,不需要额外的功能支持,也并不和实体中的其他属性相关联,这些简单的属性称为意义整体。不要“错误地” 将这些单一的属性封装成值对象类型。会有些过度。
2021-01-01 23:13:03
2021-01-02 09:43:36
标准类型 P207
- Java 的枚举。与通过状态模式来创建标准类型相比,枚举可能是更好的方法。同时得到两种方法的好处:一方面获得了一个非常简单的标准类型,另一方面又能有效地表示当前的状态。
- 被大范围使用的标准类型应该在一个单独的限界上下文中进行维护,作为消费方的限界上下文并不会维护标准类型。
值对象构造
- 值对象应该实现Serializable接口,有时是需要序列化,远程系统通信,有时是值对象序列化。
- 至少为值对象创建两个构造函数,第一个接收用于构建对象状态的所有属性参数,是主构造函数。该构造函数首先初始化默认的对象状态,对于基本属性的初始化通过调用私有的setter方法实现。该私有setter方法向我们展示了一种自委派性,是推荐的。
- 第二个构造函数用于将第一个值对象复制到另一个新的值都西昂,即复制构造函数。采用浅复制的方式,它将构造过程委派给主构造函数。先从原对象中去除各个属性值,再将这些属性值作为参数穿给主构造函数。需要深赋值或clone方法,即为每个引用的属性都创建一份其自身的备份。但是在需要的时候采用。
- 赋值构造函数对于单元测试来说是非常重要的。需要验证它的不变性。在开始之前和之后,都需要验证两个实例是否相等。
- 无参构造函数是为一些框架,如Hibernate准备的。
- 断言,对于普通软件开发和DDD模型来说都很重要。
持久化值对象
- 使用ORM将每个类映射到一张数据库表,再将每个属性映射到数据库表中的列会增加程序的复杂性。NoSQL数据库和键值对存储,具有高性能,可伸缩性,容错性和高可用性等优点。键值对存储可在很大程度上简化对聚合的持久化。
- 有时值对象需要以实体的身份进行持久化,某个值对象实例会单独占据一张表中的某条记录,而该表也是专门为该值对象类型而设计的,它甚至拥有自己的主键列。例如:当聚合中包含一组值对象的集合时。此时,一个值对象被当成了数据库实体而被持久化。
- 不要因为2,而反过来怀疑领域模型的值对象和实体对象:
- 当前所建模的概念,是表示领域中单独的一个东西,还是描述和度量其他东西
- 如该概念起描述作用,其是否满足值对象的几大特征
- 将该概念建模为实体,是否仅仅是受持久化的影响
- 将该概念建模为实体,是否是因为其具有唯一标识,我们所关注的是对象实例的个体性,是否需要在其整个生命周期的跟踪变化性。
如果答案是 “描述,yes,yes, no" 则建模为值对象
ORM
- Hibernate 和 Mysql,基本思路是将值对象与其所在的实体对象保存在同一张表中,值对象的每一个属性保存为一列,通过一个非范式的方式将值对象与实体对象保存在相同的数据库记录中,采用标准的命名约定
- Hibernate 保存值对象实例时,使用component映射元素。该元素可以用于将值对象直接映射到实体对象的数据库表中。是一种优化的序列化技术,依然可以将值对象包含在SQL查询语句中。
- 命名约定,每个property元素的列名,表示了从最上层的值对象到下层单个属性的路径指向过程。BusinessPrority 对象包含了rating对象。内层rating的属性benefit 逻辑上的指向为 businessPriority.ratings.benefit,则单个列名为 businessPriority_ratings_benefit
- 由于值对象属性通过非范式的方式保存在与实体对象相同的记录中,没有必要使用联合查询来获取实体对象,即便对于存在深层嵌套的值对象也是如此。使用HQL时,Hibernate可以简单地将对象属性表达式映射为SQL查询表达式: businessPriority.ratings.benefit 映射为business_priority_raings_benefit
Domain Event
事件存储
转发存储事件的架构风格
前提条件:领域事件被保存在了事件存储中。
两种转发事件的架构风格
- 基于REST资源的方式。并不是一种真正意义上的转发技术,但可以达到和 发布-订阅 风格相同的效果
- 基于消息中间件的方法
技术决策:
具有基本发布-订阅功能的系统环境中,采用REST风格的事件通知时最合适的。一个发布方发布的事件存在着多个消费方。另一方面,如果试图通过消息队列的方式来使用REST事件通知,就会出问题。
如果多个客户方都可以通过单个URI来请求相同的事件通知,用REST,虽然REST使用“拉”方式,而不是“推”方式。
如果一个或多个消费方需要从多个发布方中获取资源以顺序地完成一些列任务。此时不能用REST,用消息队列,许多发送方同时为一个或多个消费方服务,此时事件的接收顺序是重要的。对于实现消息队列来说,“拉” 方式并不是一个好的选择。
消息中间件 RabbitMQ 发布时间通知:fanout exchange
- 对于某个扇出交换器来说,从时间存储中查找出所有还没有被发布的领域事件对象,再将这些对象按照唯一标识升序排列。
- 依次遍历这些领域事件对象,并将它们发送给扇出交换器。
- 当消息系统成功发布事件通知之后,在扇出交换器中对该事件进行跟踪。
我们不会等待订阅方的确认信号。每个订阅方都需要自己负责处理接收到的消息。并保证其自身模型中的领域行为得到了正确的调用。
对于消息系统来说,我们只保证消息得到了投递。
实现领域事件发布
事件通知是一个应用程序级别上的关注点,而不是领域的关注点,即使这些事件源自领域模型。
聚合
关键点:聚合的不变条件和一致性边界.
原则:
1. 在一致性边界之内建模真正的不变条件.
2. 设计小聚合
3. 通过唯一标识引用其他聚合
4. 在边界之外使用最终一致性
如果是由执行该用例的客户来保证数据一致性, 则使用事务一致性.
如果需要其他用户或者系统来保证数据一致性,请使用最终一致性。
真正的系统不变条件:必须使用事务一致性的不变条件。通过领域来理解问题比纯粹的技术学习更有价值。
打破原则的理由:
1. 方便用户界面, p330
2. 缺乏技术机制:最终一致性需要使用诸如消息,定时器,或者后台线程之类的技术.
3.全局事务。 即便我们必须使用全局事务,也并不意味着我们必须在本地限界上下文中一次性修改多个聚合实例。如果可以避免全局事务,
至少我们可以在自己的模型里消除事务竞争,从而满足聚合原则。全局事务的负面影响在于系统很难有很好的伸缩性。
5. 使用迪米特法则
客户端对象调用服务对象时,应该尽量少地知道服务对象的内部结构。客户端对象不应该知道任何关于服务对象属性的信息。
任何对象的任何方法只能调用以下对象中的方法:
1.该对象自身; 2.所传入的参数对象; 3. 它所创建的对象;4. 自身所包含的其他对象, 且对那些对象有直接访问权.
6.告诉而非询问原则
客户端不应该先询问服务对象,然后根据询问结果调用服务对象中的方法,而是应该通过调用服务对象的公共接口的方式来”告诉“
服务对象所要执行的操作。
7. 避免依赖注入
通常,向聚合中注入资源库或者领域服务是有害的.
这样做的原因可能是希望在聚合内部查找一个所依赖对象的实例,所依赖的对象可能是另一个聚合,也有可能是一系列的聚合。
”原则:通过唯一标识引用其他聚合“
对于所依赖的对象,应该在聚合命令方法执行之前进行查找,然后再将其传入命令方法。
在其他多数情况下,依赖注入都是很适合的,例如,可以向应用服务中注入资源库和领域服务。
要在限界上下文中发现聚合,需要了解模型中真正的不变条件,才能决定什么样的对象可以放在一个聚合中.
不变条件指的是一个业务规则,该规则总是保持一致的。存在多种一致性,其一便是事务一致性,事务一致性要求立即性和原子性。同时还存在最终一致性。
聚合边界之内的所有内容组成了一套不变的业务规则,任何操作都不能违背这些规则.
对于一个设计良好的聚合来说,无论由于何种业务需求而发生改变,在单个事务中, 聚合中的所有不变条件都是一致的.
而对于一个设计 良好的限界上下文来说,无论在哪种情况下,它都能保证在一个事务中只修改一个聚合实例。在设计聚合时,必须将事务分析考虑在内。
不要为了对象组合上的方便而将聚合设计的很大。避免设计的聚合因为过于贫瘠而丧失了保护真正不变条件的目的。将注意力集中在业务规则上。
聚合模式讨论的是对象组合和信息隐藏,聚合模式还包含了一致性边界和事务。
在设计聚合时,我们要考虑的是聚合的一致性边界,而不是创建一个对象树。
一致性:每次客户请求应该只在一个聚合实例上执行一个命令方法.
小聚合:使用 根实体(root entity) 来表示聚合, 其中只包含最小数量的属性或者值类型属性(引用了值对象的属性).
聚合中,首先思考下,这个部分是否会随着时间而改变,或者该部分是否能被全部替换。如果可以全部替换,则建模成值对象。
优先选用值对象并不意味着聚合就是不变的,因为当值对象属性被替换成其他值对象时,根实体也就改变了。
某个用例需要修改多个聚合。对用户需求的实现是否分散在多个事务中,还是单个事务?
如果是单个事务。需要注意了,这样的用例不能准确地反映出模型中真正的聚合.
试图保持多个聚合实例间的一致性通常意味着我们缺少了某些聚合不变条件.
一个用例可能要求在单个事务中维持聚合的一致性,但是并不意味着我们就必须这么做.
业务目标可以通过聚合间的最终一致性来实现的. 包含可接受的更新延迟时间。
通过标识引用使多个聚合协同工作。
优先考虑通过全局唯一标识来引用外部聚合,而不是通过直接的对象引用.
更小的内存使用量不止在内存分配上有好处,对于垃圾回收也是有好处的。
1.在聚合中使用资源库来定位其他聚合,称为失联领域模型,事实是延迟加载的一种形式。
2.推荐另一种方法,在调用聚合行为方法之前,使用资源库或领域服务来获取所需要的对象,客户端中应用服务可以对此做出控制,然后分发给聚合.
通过应用服务来处理依赖关系可以避免在聚合中使用资源库或领域服务。
然而,如果要处理特定于领域的复杂依赖关系,在聚合的命令方法中使用领域服务却是最好的方法。
如果单次用户请求需要修改多个聚合实例,我们又需要保证模型的一致性时,这一条便非常重要了.
任何跨聚合的业务规则都不能总是保持处于最新状态。通过事件处理,批处理或者其他更新机制,可以在一定时间之内处理好地方依赖。
在一个聚合上执行命令方法时,如果还需要在其他的聚合上执行额外的业务规则,请使用最终一致性.
DDD中,一种很实用的方法可以支持最终一致性. 即一个聚合的命令方法所发布的领域事件及时地放松给异步的订阅方。
在接收到事件之后,每个订阅方都会获取自己的聚合实例,然后在该聚合上完成相应的操作。每个订阅方都在但在的事务中进行操作,即满足了
“在一次事务中只修改一个聚合实例”的原则。
如果一个订阅方与其他客户端发生了并发竞争而使修改失败。此时,订阅方并不会向消息机制发回成功确认,所以消息会重发,然后开始一个新的事务重新触发更新操作。
该过程一直持续直到一致性得到满足或者达到重试上限为止. 盖帽指数后退算法。
如果更新彻底失败,发送失败报告。
最终一致性可能会在一定程度上使用户界面变得复杂,在事件延迟的几百个毫秒期间,用户界面如何显示新的状态呢。
在显示状态时, “拉取“的方式使用Ajax,是非常低效的。 由于显示组件并不确切地知道合适应该检查状态更新,多数Ajax请求是没有必要的。
更好的方式是采用Comet Ajax 推送。
另一种方式,在界面上告诉用户,此时状态是不正确的。用户界面将定期检查状态并刷新。这样,改变之后的状态可能会在下一次界面刷新时予以显示。这是安全的。
p344
将 productBacklogItem 建模成一个实体,而非一个值对象。
采用 Hibernate 来访问数据库,对于值对象集合来说,Hibernate必须为其中的元素创建数据库实体。
对集合元素的重新排序将删除或替换掉大量的ProductBacklogItem 实例,将对基础设施造成严重的影响.
作为实体, ProductBacklogItem 允许对 ordering 属性的任意修改。
如果从 Hibernate 转向 MySQL 的键值对存储,可轻易地将 PdocutBacklogItem 变成值对象. 使用键值对
或文档存储,聚合实例通常都被序列化为一个值 展现予以存储。
乐观并发 p346
Hibernate 根实体, 双向关联. 整个聚合是通过单个值进行持久化,且该值可以避免并发冲突。
工厂
动机: 将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,却依然是领域设计的一部分。
工厂应该提供一个创建对象的接口,该接口应该封装了创建复杂对象的所有过程。
对于聚合来说,应该一次性地创建整个聚合,并且确保它的不变条件得到满足。
抽象工厂使用场景: 在一个类层级中,需要创建不同类型的对象。
客户端只需要传入一些基本的参数,抽象工厂将通过这些参数来确定需要创建的实际类型。
一个含有工厂方法的聚合根的主要职责是完成它的聚合行为。
工厂方法名能够表达通用语言。
1.有效地表达了限界上下文中的通用语言。
2.减轻客户端在创建新聚合实例时的负担。
3.确保所创建的实例处于正确的状态。
领域服务类可将其他限界上下文的对象翻译成其他限界上下文的对象。 领域服务可扮演工厂的角色。
技术上的实现,可放在合适的其他的模块中,
适配器类将其他限界上下文的 开发主机服务交互,获取相应的信息。
通过使用基于领域服务的工厂,得以将两个限界上下文中的生命周期和概念术语进行分离。
资源库
1. 定义公有接口
2.提供一种实现
对于每种需要进行全局访问的对象,我们都应该创建另一个对象来作为这些对象的提供方,就像是在内存中访问这些对象的集合一样。
为这些对象创建一个全局接口以供客户端访问.
为这些对象创建 添加 和 删除 方法, 应该提供能够按照某种 指定条件来查询这些对象的方法.
只为聚合创建 资源库.
集合一样的对象都是和持久化相关的。
每一种聚合类都将拥有一个资源库。聚合类型和资源库之间存在着一对一的关系。
当两个或多个聚合唯一同一个对象层级时,可以共享一个资源库。
只有聚合才能使用资源库,如果一个限界上下文中没有使用聚合,那么使用资源库也没有多大意义.
两种类型的资源库设计:
面向集合的设计 面向持久化 的设计
面向集合的资源库
java.util.Set 及其实现类 java.util.HashSet, 相同的对象添加两次,不会修改Set的状态,对于面向集合的资源库也是如此.
精要:
一个资源库应该模拟一个Set集合,无论采用什么类型的持久化机制,都不允许多次添加同一个实例.
当从资源库中获取一个对象并对其进行修改,并不需要"重新保存“该对象到资源库中。而是直接生效.
每一个聚合都拥有一个全局的唯一标识,该标识位于根实体,正是由于该唯一标识,类似Set的资源库才避免对同一个聚合实例的多次添加。
不应该让持久化机制通过公有接口泄露到客户端中。
背后的持久化机制必须能够隐式地跟踪发生在每个持久化对象上的改变。
包括:
1. 隐式读时复制 ( Implicit Copy-on-Read )
从数据存储 中读取一个对象时,持久化机制隐式地对该对象进行复制,在提交时,再将该复制对象与客户端中的对象进行比较。
当客户端请求持久化机制从数据存储中读取一个对象时,该持久化机制一方面将获取到的对象返回给客户端,一方面立即创建一份该对象的备份
(去除延迟加载部分,这些部分可以在实际加载时再进行复制 ).
当客户端提交事务时,持久化机制把该复制对象与客户端中的对象进行比较。所有的对象修改都将更新到数据存储中。
2. 隐式写时复制 Implicit Copy-on-Write
持久化机制通过委派来管理所有被加载的持久化对象。在加载每个对象时,持久化机制都会为其创建一个微小的委派并将其交给客户端。
客户端并不知道调用的是委派对象中的行为方法,委派对象会调用真实对象中的行为方法。
当委派对象首次接收到方法调用时,它将创建一份对真实对象的备份。
委派对象将跟踪发生在真实对象上的改变,并将其标记为”dirty", 当事务提交时,该事务检查所有的‘dirty' 对象并将它们的修改更新到数据存储中。
Hibernate 能够允许我们创建一个传统的,面向集合的资源库。
高性能 ORM : Oracle 的 TopLink, 提供了 Unit of Work 功能.
不提供隐式读时复制功能,采用 显式写前复制 Explicit Copy-before-Write , 这里的 “显示” 表示, 在修改对象之前,客户端必须通知Unit of Work,
在接到通知之后,Unit of Work 便会克隆相应的领域对象以做好修改准备。 好处在于,TopLink 只会在需要的时候才会占用内存。
Hibernate 的 Session 同时也是一个 Unit of work.
面向持久化资源库
如果持久化机制不支持对象变化的跟踪,无论式隐式的还是显示的,那么采用面向集合资源库便不再适用了。
面向持久化资源库,基于保存操作的资源库,在使用数据网织或者NoSQL键值对存储时,每次新建聚合或者修改聚合之后,都需要调用资源库中的
save() 方法或者值类的方法。
精要:
在向存储库中添加新建对象或者修改既有对象时,都必须显示地调用put方法,该方法将以新建的值来替换先前关联在某个键的原值。
这种类型的数据存储可以极大地简化对聚合的读写。正因如此。
这种数据存储也被称为聚合存储 Aggregate Store 或面向聚合的存储 Aggregate-Oriented Database.
Oracle Coherence, 内存使用的时类似于 HashMap 的 Map 实现。每一个元素被称为 Entry
MongoDB Riak, 以键值对存储数据。具有Map特征。使用的是磁盘而不是内存存储。
使用 put() ,即使被修改对象和既有对象在逻辑表示同一个对象,替换依然发生。每一个put() 和 putAll() 方法都表示一个单独的逻辑事务。
这些持久化机制通常不会提供Unit of Work, 或并不提供事务边界以对原子的写操作进行控制。
这些数据存储极大地简化了对聚合的基本读写操作。
Java 标准的序列化机制,会向每一个对象添加额外的字节。性能也较低。
不得不降低数据网织所能缓存对象的数目,便不能享受数据网织的高性能了。
事务管理
事务的管理绝对不应该放在 领域模型 和 领域层中. 与领域模型相关的操作都是非常细粒度的,以致于无法使用事务。
另外,领域模型也不应该意识到事务的存在。(所讨论的事务主要是关于 RDBMS的)
要将事务放在应用层中,然后为每个主要的用例创建一个 门面, 门面中的业务方法通常都是粗粒度的,常见的情况是每一个用例流
对应一个业务方法。
业务方法对用例所需操作进行协调。
当用户界面层调用门面中的一个业务方法时,该方法都将开始一个事务。同时,该业务方法将作为领域模型的客户端而存在。
在所有的操作完成之后,门面中的业务方法将提交事务。这个过程中,如果发生异常/错误,那么业务方法将对事务进行回滚.

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




对于任何一个持久化机制,都需要在整个业务操作中访问相同的 Session, Unit of Work 和由应用层所管理的事务。
不要过度地在领域模型上使用事务,必须慎重地 设计聚合以保证正确的一致性边界。
资源库 VS DAO
资源库,ORM, 和 DAO 都提供了对持久化机制的抽象.
我们不能将所有的持久化抽象都称为DAO,而是需要确定这种模式是否得到了真正的实现.
资源库和数据映射器 Data Mapper 更偏向于对象,通常用于领域模型.
DAO 主要是从数据库表的角度来看待问题,且提供CRUD操作,诸如 表模块(Table module) , 表数据网关(Table data gateway) 和活动记录 (Active Record)
这样的模式应该用于事务脚本程序中。这是因为,这些与DAO相关的模式通常只是对数据库表的一层封装.
DAO 模式中所执行的CRUD 操作都可以放在聚合中来实现,希望由 聚合本身来管理业务逻辑,避免在领域模型中使用这些 DAO 模式
在数据存储中放置以及执行业务逻辑是与DDD背道而驰。
大量地使用存储过程则是具有分裂性.
在设计资源库时,应该采用面向集合的方式,而不是面向数据访问的方式。有助于将自己的领域当模型看待,而不是CRUD操作。
测试资源库
两个方面:
1. 需要测试资源库本身时能正确工作的。 必须使用产品环境下的资源库实现。
2. 测试对资源库的使用,以保证能够正确地保存和获取聚合实例。既可以使用产品实现,也可以使用内存实现。
测试注意清理工作,避免缓存的影响。
1. 保存一个product 实例,然后再进行查找。首先,实例化一个product,并将其保存到资源库中,在没有发生异常的情况下,便可以认为保存操作执行成功。
要确认这一点: 从资源库中找到该实例,再将其与原来 的product实例进行比较。
接收product 的唯一标识作为参数查找。
2。测试多个product 实例,调用saveAll() 一次性全部保存。然后依然通过 productOfId() 分别查找单个实例。
集成限界上下文
1. 通过REST资源的方式来集成限界上下文。
2. 通过消息来集成限界上下文。
多种直接的方式来完成限界上下文之间的集成.
1. 在一个限界上下文中暴露应用程序编程接口 API. 然后在另一个限界上下文中通过远程过程调用 RPC 的方式访问该 API, 此时的API 可以通过
SOAP 协议暴露给其他限界上下文,也可以直接在 HTTP 中使用 XML (这种方式与 REST不同)。还有多种方式。
2. 使用消息机制。消息机制,每一个需要交互的系统都是用消息队列或发布-订阅机制,将消息机制看成是一种服务接口。
3. REST ful HTTP,在REST 的请求中不包含过程调用中的参数,从一个系统向另一个系统发出请求。REST 用于交换和更改资源,这些资源是通过URI
的方式进行定位,每种资源的URI都是唯一的.在每种资源上,可以执行不同种类的操作。 GET, PUT 和 DELETE,POST, 通过这4个方法的意图对不同的操作分类。
GET方法包含不同类型的查询操作, PUT 方法则用于聚合上的命令操作。
共享文件 和 数据库的方式 太不与时俱进了。
RPC不支持 自治性服务,如果RPC提供方失效,客户方调用也将失败。
分布式系统之间存在根本性区别
- 网络不可靠的.
- 总会存在时间延迟,有时甚至非常严重.
- 带宽是有限的.
- 不要假设网络是安全.
- 网络拓扑结构将发生变化.
- 知识和政策在多个管理之间传播.
- 网络传输是有成本的.
- 网络是异构的.
产品环境下,低耦合的自定义媒体类型契约会好于 部署接口/类. 自定义的媒体类型契约 和 NotificationReader.
开发主机服务:当一个限界上下文以 URI 的方式提供了大量的 REST 资源时,便可称其为开发主机服务:
为系统所提供的服务定义一套协议。开放该协议以使其他需要集成的系统能够使用。在有新的集成需求时,对协议进行改进和扩展.
HTTP方法-GET,PUT, POST,DELETE -- 以及它们所操作的资源看作时开放的服务。HTTP 和 REST 便组成了交互系统之间的开放协议;
而取之不竭的 URI 又使得这些协议能够处理新的集成需求.
使用这种方式的客户端并不是完全自治的。由于在请求服务时,REST服务 的提供方都必须直接 参与。
如果提供REST服务的限界上下文不可用,那么客户端限界上下文也无法完成集成操作.
弥补办法:定时器或消息机制 来营造一种暂时的解耦。
系统可在定时器触发时,或者事件到达时,与远程系统交互。如果远程系统不可用,定时器所获得的服务数据便可以作为替补。
或者使用消息时,可以向消息提供方回复否定回答,以使其重新发布消息。
Restful HTTP 这种方式的好处在于不用向客户方暴露模型的结构和行为细节,需要通过REST 资源的方式向外提供服务的数据信息。
类似 URI 发出 GET 请求 :
/tenants / { tenantId } / users / { username } /inRole / { role }
客户方想知道某个租户下的某个用户是否扮演了某个角色
如果该用户的确扮演了role这个角色,在返回中将包含HTTP的状态码 200,表示成功,
否则将返回表示无内容的状态码 204。 这是一种简单的 RESTful HTTP 设计。
从集成方所需用例(用户故事)的角度来看待问题。符合开放主机服务的部分定义。
向集成方隐藏领域模型的细节。增加了那些依赖方限界上下文的可维护性.
使User-Role 形式的数据能够服务于上下文,适配器,协作上下文的特定的接口和类,xxService, xxAdapter,xxxTranslator,
HttpClient--通过 JAX-RS 实现的 ClientRequest 和 ClientResponse 实现。
独立接口 CollaboratorService 当作领域模型的一部分,并将它放在六边形的内部,但它的实现使技术性的,并且被放置在
六边形架构的外部,即端口和适配器所在的位置.
作为技术实现的一部分,在防腐层中通常会有一个特定的 适配器 和 翻译器
Jboss restEasy
通过消息集成限界上下文
DDD中, 增强系统自治性的一种方式便是使用领域事件。当一个系统中发生一些显著的事情时,它将为此发布领域事件。
每个系统都存在着多个甚至大量的事件,在创建这些事件时,我们需要考虑到事件的唯一性以便对每个事件进行记录.
使用消息进行集成时,任何一个系统都可以获得更高层次的自治性.只要消息基础设施工作正常,
即使其中一个交互系统不可用,消息依然可以得到发送和投递.
发布方和订阅方都应该使用事件的全类名,其中包含了事件所在的模块名和事件本身的类名。
这样,如果不同的限界上下文使用了相同的事件类名.
盖帽指数后退算法。 失败重复发送. 1: 1-2 ; 2: 1-4; 3: 1-8; 4: 1-16 指数增长。
1. 可靠的消息机制,有可能重复投递同一条消息。一旦消息被发送到交换器中,该消息必然会被监听器所监听到。
如果在创建写作对象时发生了延迟,进而导致了消息的重发,将导致多次发送同一个xx命令对象的情况。
2. 消息重试禁用的底线时,将协作上下文的操作变成幂等操作。避免重复创建
3.对命令的重发知道成功为止,否则,协作上下文无法成功创建Forum 和 Discussion 对象。‘
如果消息发送失败,可以抛出一个异常,引发一个否定应答。之后RabbitMQ 重发xx事件通知,Listener将再次监听到该事件。
另一种方法则是重复发送直到成功,盖帽指数后退算法。但如果RabbitMQ不可用,则很长时间里都无法成功对消息重发。
更复杂的长时处理过程
在需要多部完成的情况下,最好采用一个状态机。创建一个process. p460
如果RabbitMQ不可用,有可能在很长一段事件里都无法成功地对消息进行重发。因此,将否定应答和消息重发结合起来使用
可能是最好的方式. 如果发生彻底超时的情况,系统将发送一封E-Mail以请求人为干预.
使用抽象基类AbstractProcess,作为一个适配器来使用。对于开发更加复杂的长时处理过程来说,扩展子Entity基类,
可以简单地将一个聚合设计成process,必如,使Product继承自AbstractProcess,虽然它并不需要这样的复杂度. P463
当消息机制或你的系统不可用时
不足之处在于:一段时间之内,它有可能是不可用的.此时需要注意的是。
1. 在消息机制不可用时,通知的发布方将不能通过该消息机制发布事件。这种情况将被发布客户端所检测到,此时的客户端可以退一步,等消息系统可用时
再进行正常发送,如果其中一次发送成功,便可以认为消息系统已经可用了,请缺少消息的发送频率小于正常情况,可以每隔30秒或者1分钟重试一次。
请注意,如果你的系统使用了事件存储,那么你的事件再成功发送之前都将一致位于消息队列中,当消息系统可用是,立即发送消息。
2. 对于消息监听器来说,消息机制不可用时,它将接收不到新的事件通知。当消息系统重新可用时,你的监听器会被自动地重新激活吗,也许你需要重新
进行订阅?如果此时的消费方不能自动恢复,你需要确保重新注册该消费方。否则你将发现你的限界上下文将不再接收所依赖限界上下文发出的通知,
这是需要避免的。
问题不总是出在消息机制。当你的限界上下文变得不可用,当它再次可用时,此时的消息系统已经收集到了大量的为投递消息。
然后,你的限界上下文重新注册消息的消费方,那么要接收并处理完所有未被处理的消息将消耗大量的时间。
可增加更多的节点(集群)。无法避免停机的情况。 p464
2021-01-09 22:11:41
应用程序
虚线表示依赖注入原则,而实现表示操作分发。
基础设施实现了用户界面,应用服务和领域模型中的抽象接口,同时它还将操作分发给应用服务,领域模型和数据存储。

用户界面通常需要渲染多个聚合实例中的属性,尽管用户最终只会修改其中一个聚合实例.
渲染数据传输对象。
用例优化资源库查询
与其读取多个聚合实例,然后再通过编程的方式将它们组装到单个容器DTO或DPO中,我们可以转而使用 用例优化查询。
在资源库中创建一些查询方法,这些方法返回的是所有聚合实例属性的超集。
查询方法动态地将查询结果放在一个值对象中,该值是特别为当前用例设计的。
你设计的是值对象,而不是DTO. 因为此时的查询时特定于领域的,而不是特定于应用程序的。
这个用例优化的值对象将被直接用于渲染用户界面.
用例优化查询的动机与 CRRS 相似, 然而,用例优化查询依然会使用资源库,而不会直接与数据库打交道,比如使用SQL。
DTO
一种渲染多个聚合实例的方法时 数据传输对象 Data Transfer Object, DTO, DTO将包含需要显示的所有属性值。
应用服务通过资源库读取所需的聚合实例,然后使用一个 DTO 组装其 DTOAssemble 将需要显示的属性值映射到 DTO 中,之后,
用户界面组件将访问每一个DTO属性值,并将其渲染到显示界面中,对数据的读写都是通过资源库完成的。好处是:
1.不会存在延迟加载的问题,因为DTO Assemeble 会直接访问聚合中需要用来创建 DTO 的所有数据。
2.可以解决 Persistation Tier 和 业务层 Business Tier 存在物理分离的情况,此时我们需要对数据进行序列化,然后通过
网络将其传输到展现层中.
DTO 模式本来就是用于再远程的展现层中显示数据的。此时,DTO 在业务层中创建,再序列化,然后通过网络发送,最后在
展现层中反序列化。
如果你的展现层不是远程的,则不要使用。
缺点在于:
1. 需要创建一些与领域对象非常相似的类。
2.需要创建一些必须由虚拟机如 JVM 所管理的大对象,事实上这些对象却与但虚拟机应用架构不相匹配。
在使用DTO 时,我们的聚合设计需要考虑到 DTO Assemble 对聚合数据的查询。不应该暴露出太多的聚合内部结构。
应尽量将客户端从聚合的内部状态中解耦。不应该使客户端--即DTO Assemble 深度访问聚合的状态。使客户端与聚合实现
紧密地耦合起来。
使用调停者发布聚合的内部状态
要解决客户端和领域模型之间的耦合问题,可以使用调停者模式,即双分派 Double-dispatch 和 回调 callBack。
此时,
1.聚合将通过调停者接口来发布内部状态。
2.客户端将实现调停者接口,然后把实现对象的引用作为参数传给聚合。
聚合双分派给调停者以发布自身状态,在这个过程中,聚合并没有向外暴露自身的内部结构。
诀窍在于:不要将调停者接口与任何显示规范绑定在一起,而是关注于对所感兴趣的聚合状态的渲染。
不同的兴趣提供方可以通过其他类来实现.
DPO domain payload object.
优点在于可以用于单虚拟机应用架构中。DPO 包含了对整个聚合实例的引用,而不是单独的属性。
聚合实例集群可以在多个逻辑层之间传输。应用服务通过资源库获取到所需聚合实例,然后创建DPO实例,该DOP持有对所有聚合实例
的引用。之后,展现组件通过DPO获得聚合实例的引用,再从聚合实例访问需要显示的属性.
优点在于:简化了在不同逻辑层之间传输集群数据的过程。更容易设计,且消耗更少的内存.
在创建DPO之前,由于聚合实例必须被读到内存中,因此之后在使用DPO时,这些聚合实例已经存在了.
如果要避免 用户界面和 模型之间的耦合,需要使用 调停者。
情况:由于DPO持有的是对整个聚合实例的引用, 延迟加载的对象/集合并未加载到内存中。在创建DPO时,我们没有必要
访问所需的聚合属性。由于在应用服务的方法结束时,事务已随之提交,之后在展现组件中访问那些延迟加载的属性时,
程序抛出异常。
要解决延时加载的问题,可以选择即时加载,使用 领域依赖求解器 Domain dependency resolver, DDR, 是一种策略模式。
通常对于每一个用例流,都会使用一种策略。
对于某个用例流所需要的所有延迟加载属性,对应的策略都会强制性地对其进行访问。
这样的访问策略作用域应用服务提交事务并返回 DPO 之前。可以对这样的策略进行硬编码以手动的访问所有延迟加载属性,
或者使用简单的表达式语言,并通过反射的机制来访问这些属性.
聚合实例的状态展现
如果你的程序提供REST资源,那么你便需要为领域模型创建状态展现以供客户端使用。
我们应该基于用例来创建状态展现,而不是基于聚合实例。非常重要!
这一点来看,创建状态展现和DTO是相似的。DTO也是基于用例的。
然而,更准确的是将一组REST资源看作一个单独的模型--视图模型 View Model 或 展现模型 Presentation Model。
我们所创建的展现模型不应该于领域模型中的聚合状态存在一一对应的关系.
应用服务
应用服务是领域模型的直接客户,应用服务负责用例流的任务协调。每个用例流对应了一个服务方法。
在使用ACID数据库时,应用服务还负责控制事务以确保对模型修改的原子提交。
应该将所有业务领域逻辑放在领域模型中,不管是聚合,值对象或者领域服务;而将应用服务做成很薄的一层,且使用
它们来协调对模型的任务操作.
将领域对象暴露给不同类型的客户端,每种客户端都需要单独地处理这些对象类型。耦合问题将严重。
使用数据转换器作为返回类型。
@Transactional
可写,当客户端从Spring 容器获取到该TenantIdentityService并调用服务方法时,事务便会启动。
当这些方法正常返回时,事务将被提交。根据不同配置,从方法中抛出的异常将导致事务的回滚。
安全 Spring Security @PreAuthorize("hasRole('xxx')")
基础设施
从架构上来看,基础设施职责是为应用程序的其他部分提供技术支持。保持着依赖倒置原则。
无论基础设施位于什么地方,只要它的组件依赖于用户界面,应用服务和领域模型中的接口,而这些接口又需要特殊的技术支持,
那么它都能工作的很好。
在应用服务获取资源库时,它只会依赖于领域模型中的接口,而实际使用的则是基础设施中的实现类。

对资源库的查找可用 依赖注入 或者 服务工厂。
资源库的实现被放在了基础设施层中,因为它们负责处理数据存储,而这些不属于模型的职责。
可以使基础设施实现那些与消息有关的接口,比如消息队列和E-MAIL,
如果还有一些特殊的用户界面组件来处理注入图表之类的展现,也应该放在基础设施层中.
2021-01-10 22:16:55 《IDDD 实现领域驱动设计》 第一遍通读完成。

浙公网安备 33010602011771号