元数据

实现领域驱动设计

  •  实现领域驱动设计|200
  • 书名: 实现领域驱动设计
  • 作者: 沃恩·弗农
  • 简介: 本书分别从战略和战术层面详尽地讨论了如何实现DDD,其中包含了大量的最佳实践、设计准则和对一些问题的折中性讨论。全书共分为14章,在DDD战略部分,本书向我们讲解了领域、限界上下文、上下文映射图和架构等内容,战术部分包括实体、值对象、领域服务、领域事件、聚合和资源库等内容。一个虚构的案例研究贯穿全书,这对于实例讲解DDD实现来说非常有用。
  • 出版时间 2014-03-01 00:00:00
  • ISBN: 9787121224485
  • 分类: 计算机-数据库
  • 出版社: 电子工业出版社

高亮划线

总览

  • 📌 创建支撑子域的原因在于它们专注于业务的某个方面,否则,如果一个子域被用于整个业务系统,那么这个子域便是通用子域。 ^33380964-20-7535-7626
    • ⏱ 2022-06-04 07:45:30

战略设计为什么重要

  • 📌 问题在于论坛和讨论等概念与错误的语言概念耦合起来了。用户和权限与协作活动没有任何关系,并且与协作的通用语言也风牛马不相及。用户和权限是与身份(Identity)和访问(Access)相关的概念,即是与安全(Security)相关的。在协作上下文(Collaboration Context)中出现的每一种概念都必须与协作存在语言层面上的关联,而现在它们没有。“我们应该关注的是协作概念,比如作者(Author)和主持者(Moderator),这些才是协作活动中的正确概念和语言。” ^33380964-21-1444-1684

    • ⏱ 2022-06-04 07:57:03
  • 📌 重申一遍,SaaSOvation的开发者在一开始并没有意识到用户和权限是与协作工具无关的概念。诚然,他们的软件中是有用户的,但是我们应该将不同的用户种类区别对待,因为在不同的上下文中他们所完成的任务是不一样的。在协作工具中,我们更关注的是用户的角色,而不是他们是谁或者他们的权限如何。然而,在当前这个例子中,SaaSOvation的开发人员将协作模型与用户和权限完全揉合在一起了,如果系统对用户或权限的处理方式有所修改,这也将导致对协作模型的修改。 ^33380964-21-1997-2221

    • ⏱ 2022-06-04 08:03:15
  • 📌 论坛和“谁可以发表帖子,还有在什么条件下可以发表”其实没有多大关系。论坛只需要知道“有作者正在发表帖子,或者有作者曾经发表过帖子”就可以了。于是团队成员学到了:决定谁可以做什么事情其实是由另外一个完全不同的模型来负责的,在协作模型中,我们只需要知道这样的问题已经被回答过就行了。 ^33380964-21-2335-2474

    • ⏱ 2022-06-04 08:05:44

理解限界上下文

  • 📌 限界上下文是一个显式边界,领域模型便存在于边界之内。在边界内,通用语言中的所有术语和词组都有特定的含义 ^33380964-23-665-716

    • ⏱ 2022-06-08 19:11:11
  • 📌 领域模型把通用语言表达成软件模型。 ^33380964-23-459-476

    • ⏱ 2022-06-08 19:11:49
  • 📌 限界上下文主要是一个语义上的边界 ^33380964-23-833-849

    • ⏱ 2022-06-08 19:12:21
  • 📌 需要集成时,我们必须在不同的限界上下文之间进行概念映射。 ^33380964-23-3194-3222

    • ⏱ 2022-06-08 19:14:48
  • 📌 我们应该为每个阶段创建各自的限界上下文。在每个限界上下文中,都存在某种类型的图书(Book)。在几乎所有的上下文中,不同类型的图书对象将共享一个身份标识(identity),这个标识可能是在概念设计阶段创建的。然而,不同上下文中的图书模型却是不同的。当某个限界上下文的团队说到图书时,该“图书”正好能表示该上下文所需要的意思。 ^33380964-23-4231-4394

    • ⏱ 2022-06-08 19:17:55
  • 📌 一个限界上下文并不是只包含领域模型。诚然,模型是限界上下文的主要“公民”。但是,限界上下文并不只局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务[3]。有时,限界上下文所包含的内容可能比较少,比如,一个通用子域便可以只包含领域模型。 ^33380964-23-5950-6197

    • ⏱ 2022-06-08 19:23:27
  • 📌 限界上下文主要用来封装通用语言和领域对象,但同时它也包含了那些为领域模型提供交互手段和辅助功能的内容。 ^33380964-23-7975-8026

    • ⏱ 2022-06-08 19:29:27
  • 📌 核心领域之外的概念不应该包含在限界上下文中。 ^33380964-23-8674-8696

    • ⏱ 2022-06-08 19:31:56
  • 📌 那么,哪些因素会导致我们创建大小不正确的限界上下文呢?我们可能错误地采用架构来指导设计开发,而不是通用语言 。一些平台、框架或者基础设施通常是用来打包和部署组件的,它们可能影响我们对限界上下文的设计,此时我们会从技术层面而不是语义边界来考虑问题。 ^33380964-23-9922-10045

    • ⏱ 2022-06-08 19:34:09
  • 📌 将限界上下文想成是技术组件并无大碍,只是我们需要记住:技术组件并不能定义限界上下文。 ^33380964-23-11168-11210

    • ⏱ 2022-06-08 19:37:26

示例上下文

  • 📌 简单来讲,隔离内核就是将所有与安全和权限相关的类迁移到一个单独的模块中。然后,在调用核心领域逻辑之前,使用应用服务去检查用户的安全权限。这样,在核心域中只需要实现和协作行为相关的功能。应用服务负责安全权限检查和对象转换 ^33380964-24-5463-5572
    • ⏱ 2022-06-08 20:38:37

上下文映射图为什么重要

  • 📌 上下文映射图主要帮助我们从解决方案空间的角度看待问题。 ^33380964-27-946-973

    • ⏱ 2022-06-09 09:28:19
  • 📌 并不是说自治服务就可以完全独立于上游模型,而是我们的设计应该尽可能地限制实时依赖性。 ^33380964-27-13411-13453

    • ⏱ 2022-06-09 09:54:17
  • 📌 为了达到比RPC更高的自治性,敏捷项目管理上下文的团队将尽量限制对RPC的使用,此时他们可以选择异步请求,或者事件处理等方式。 ^33380964-27-17573-17636

    • ⏱ 2022-06-09 10:10:33
  • 📌 在本地创建一些由外部模型翻译而成的领域对象,这些对象保留着本地模型所需的最小状态集。为了初始化这些对象,我们只需要有限的RPC调用或REST请求。然而,要与远程模型保持同步,最好的方式是在远程系统中采用面向消息的通知(notification)机制。消息通知可以通过服务总线进行发布,也可以采用消息队列或者REST。 ^33380964-27-17738-17896

    • ⏱ 2022-06-09 10:12:15
  • 📌 在使用领域事件(8)和事件驱动架构(Event-Driven Architecture,4)时,我们应该仔细思考最终一致性(Eventual Consistency)。本地系统产生的事件通知并不是只能由远程系统消费。在ProjectOvation中,当ProductInitiated事件产生时,该事件将由本地系统进行处理。本地系统要求Forum和Discussion在远程完成创建,这可以通过RPC或消息机制完成,当然这取决于CollabOvation支持哪种类型的通信方式。在使用RPC时,如果远程的CollabOvation系统不可用,PorjectOvation将定期重试直到成功为止。如果采用消息机制,ProjectOvation将向CollabOvation发出消息,在资源创建成功之后,CollabOvation同样会以消息的形式返回。当ProjectOvation接收到返回的消息时,它将使用新建Discussion的标识引用来更新本地的Product对象。 ^33380964-27-22112-22584

    • ⏱ 2022-06-09 11:03:28

分层

  • 📌 依赖倒置原则由Robert C. Martin提出[Martin,DIP],正式的定义为:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。根据该定义,低层服务(比如基础设施层)应该依赖于高层组件(比如用户界面层、应用层和领域层)所提供的接口。 ^33380964-31-5111-5362

    • ⏱ 2022-06-10 10:44:08
  • 📌 如果应用服务比上述功能复杂许多,这通常意味着领域逻辑已经渗透到应用服务中了,此时的领域模型将变成贫血模型。因此,最佳实践是将应用层做成很薄的一层。 ^33380964-31-2926-2999

    • ⏱ 2022-06-10 10:45:28
  • 📌 应用服务是表达用例和用户故事(user story)的主要手段。 ^33380964-31-2582-2614

    • ⏱ 2022-06-10 10:54:43

六边形架构(端口与适配器)

  • 📌 通常来说,我们都不用自己实现端口。我们可以将端口想成是HTTP,而将适配器想成是Java的Servlet或JAX-RS的REST请求处理类。或者,我们可以为NServiceBus或RabbitMQ创建消息监听器,在这种情况下,端口是消息机制,而适配器则是消息监听器,因为消息监听器将负责从消息中提取数据,并将数据转化为应用层API(领域模型的客户)所需的参数。 ^33380964-32-2308-2488

    • ⏱ 2022-06-10 10:41:41
  • 📌 可以将资源库的实现看作是持久化适配器 ^33380964-32-3669-3687

    • ⏱ 2022-06-10 11:19:12

命令和查询职责分离——CQRS

  • 📌 现在,对于同一个模型,考虑将那些纯粹的查询功能从命令功能中分离出来。聚合将不再有查询方法,而只有命令方法。资源库也将变成只有add()或save()方法(分别支持创建和更新操作),同时只有一个查询方法,比如fromId()。这个唯一的查询方法将聚合的身份标识作为参数,然后返回该聚合实例。资源库不能使用其他方法来查询聚合,比如对属性进行过滤等。在将所有查询方法移除之后,我们将此时的模型称为命令模型(Command Model)。但是我们仍然需要向用户显示数据,为此我们将创建第二个模型,该模型专门用于优化查询,我们称之为查询模型(Query Model)。 ^33380964-35-1776-2055

    • ⏱ 2022-06-14 00:30:45
  • 📌 图4.6 在CQRS中,来自客户端的命令通过单独的路径抵达命令模型,而查询操作则采用不同的数据源,这样的好处在于可以优化对查询数据的获取,比如用于展现、用于接口或报告的数据。 ^33380964-35-3172-3300

    • ⏱ 2022-06-14 00:32:17

事件驱动架构

  • 📌 表4.2 基于消息的管道和过滤器处理过程的基本特征 ^33380964-36-3633-3658

    • ⏱ 2022-06-19 10:32:49
  • 📌 我们可以对上面的管道和过滤器的例子进行扩展,从而得到另一种事件驱动的、分布式的并行处理模式——长时处理过程(Long-Running Process)。一个长时处理过程有时也称为Saga, ^33380964-36-6123-6252

    • ⏱ 2022-06-19 10:48:11
  • 📌 有人认为执行器和跟踪器这两种概念合并成一个对象——聚合——是最简单的方法。此时,我们在领域模型中实现这样一个聚合,再通过该聚合来跟踪长时处理过程的状态。 ^33380964-36-9306-9382

    • ⏱ 2022-06-19 11:17:09
  • 📌 在六边形架构中,端口-适配器的消息处理组件将简单地将任务分发给应用服务(或命令处理器),之后应用服务加载目标聚合,再调用聚合上的命令方法。同样,聚合也会发出领域事件,该事件表明聚合已经完成了它的处理任务。 ^33380964-36-9477-9579

    • ⏱ 2022-06-19 11:20:14
  • 📌 然而,将执行器和跟踪器分开讨论是一种更有效的方法。 ^33380964-36-9711-9736

    • ⏱ 2022-06-19 11:23:06
  • 📌 在实际的领域中,一个长时处理过程的执行器将创建一个新的类似聚合的状态对象来跟踪事件的完成情况。该状态对象在处理过程开始时创建,它将与所有的领域事件共享一个唯一标识。同时,将处理过程开始时的时间戳保存在该状态对象中也是有好处的 ^33380964-36-9765-9877

    • ⏱ 2022-06-19 11:24:06
  • 📌 当并行处理的每个执行流运行完毕时,执行器都会接收到相应的完成事件。然后,执行器根据事件中的过程标识获取到与该过程相对应的状态跟踪对象实例,再在这个对象实例中修改该执行流所对应的属性值。 ^33380964-36-9940-10032

    • ⏱ 2022-06-19 11:26:34
  • 📌 长时处理过程的状态实例通常有一个名为isCompleted()的方法。每当某个执行流执行完成,其对应的状态属性也将随之更新,随后执行器将调用isCompleted()方法。该方法检查所有的并行执行流是否全部运行完毕。当isCompleted()返回true时,执行器将根据业务需要发布最终的领域事件。 ^33380964-36-10413-10563

    • ⏱ 2022-06-19 11:36:56
  • 📌 当一个完成事件到达时,执行器将检查该事件中相应的状态属性,该状态属性表示该事件是否已经存在。如果状态已经被设值,那么该事件便是一个重复事件,执行器将忽略该事件,但是还是会对该事件做出应答[8]。另一种方式是将状态对象设计成幂等的。这样,如果执行器接收到了重复消息,它将同等对待,即执行器依然会使用该消息来更新处理过程的状态,但是此时的更新不会产生任何效果。在以上两中方法中,虽然只有第二种方法将状态对象本身设计成幂等的,但是在结果上他们都能达到消息传输的幂等性。 ^33380964-36-10918-11271

    • ⏱ 2022-06-19 11:44:13
  • 📌 被动超时检查由执行器在每次并行执行流的完成事件到达时执行。执行器根据状态跟踪器来决定是否出现超时,比如调用名为hasTimedOut()的方法。如果执行流的处理时间超过了最大允许处理时间,状态跟踪器将被标记为“遗弃”状态。此时,执行器甚至可以发布一个表明处理失败的领域事件。被动超时检查的一个缺点是,如果由于某些原因导致执行器始终接收不到完成领域事件,那么即便处理过程已经超时,执行器还是会认为处理过程正处于活跃状态。如果还有更大的并发过程依赖于该过程处理,那么这将是不可接受的。 ^33380964-36-11513-11753

    • ⏱ 2022-06-19 11:47:37
  • 📌 对于跟踪有些长时处理过程来说,我们需要考虑时间敏感性。在过程处理超时,我们既可以采用被动的,亦可以采取主动。 ^33380964-36-11358-11412

    • ⏱ 2022-06-19 11:47:56
  • 📌 主动超时检查可以通过一个外部定时器来进行管理。 ^33380964-36-11782-11805

    • ⏱ 2022-06-19 11:49:34
  • 📌 在处理过程开始时,定时器便被设以最大允许处理时间。定时时间到时,定时监听器将访问状态跟踪器的状态。如果此时的状态显示处理还未完毕,那么处理状态将被标记为“遗弃”状态。如果此时处理过程已经完毕,那么我们可以终止定时。主动超时检查的一个缺点是,它需要更多的系统资源,这可能加重系统的运行负担。同时,定时器和完成事件之间的竞态条件有可能会造成系统失败。 ^33380964-36-11847-12020

    • ⏱ 2022-06-19 11:50:31
  • 📌 长时处理过程通常和分布式并行处理联系在一起,但是它与分布式事务没有什么关系。长时处理过程需要的是最终一致性。 ^33380964-36-12049-12103

    • ⏱ 2022-06-19 11:51:13
  • 📌 有时,我们的业务可能需要对发生在领域对象上的修改进行跟踪。 ^33380964-36-13259-13288

    • ⏱ 2022-06-19 11:58:32
  • 📌 这些需求要求维护一份审计日志(Audit Log)。然而,审计日志也是存在局限的,虽然它包含了系统中的一些事件发生信息,甚至可以用于系统调试,但是我们并不能检查单个领域对象在改变前和改变后的状态。 ^33380964-36-13489-13587

    • ⏱ 2022-06-19 12:01:25
  • 📌 图4.11 从高层次看事件源,由聚合发布的事件被保存到事件存储中,同时这些事件被用于跟踪模型的状态变化。资源库从事件存储中读取事件,并将这些事件应用于对聚合状态的重建。 ^33380964-36-14429-14513

    • ⏱ 2022-06-19 12:05:00
  • 📌 我们这里所说的事件源是指:对于某个聚合上的每次命令操作,都有至少一个领域事件发布出去,该领域事件描述了操作的执行结果。每一个领域事件都将被保存到事件存储(Event Store,8)中。每次从资源库中获取某个聚合时,我们将根据发生在该聚合上的历史事件来重建该聚合实例,事件的作用顺序应该与它们的产生顺序相同[10]。 ^33380964-36-14574-14891

    • ⏱ 2022-06-19 12:07:06
  • 📌 为了避免这种瓶颈,我们可以通过聚合状态“快照”的方式来进行优化。我们可以创建一个聚合内存状态的快照,此时的快照反映了聚合在事件存储历史中某个事件发生后的状态。为了达到这样的目的,我们需要利用该事件及其发生前的所有事件来重建聚合实例,之后对聚合状态进行序列化,再把序列化之后的快照保存在事件存储中。这样一来,我们便可以先通过聚合快照来实例化某个聚合,接着再“重放”比快照更新的事件来修改聚合的状态,最终使聚合达到最后一个事件发生后的状态。 ^33380964-36-15361-15579

    • ⏱ 2022-06-19 12:15:28
  • 📌 事件通常以二进制的方式保存在事件存储中,这使得事件源不能用于查询操作。事实上,为事件源所设计的资源库只需要一个接受聚合唯一标识为参数的查询方法。因此,我们需要另外一种方法来支持查询,通常将CQRS和事件源一同使用[11]。 ^33380964-36-15836-16071

    • ⏱ 2022-06-19 12:17:52
  • 📌 有了所有事件的历史信息,业务层便可以考虑很多诸如“如果……会怎么样?”的问题。即通过重放一组发生在聚合上的事件,业务层可以得到很多问题的答案。 ^33380964-36-16597-16668

    • ⏱ 2022-06-19 12:22:13
  • 📌 有些消息机制中已经内建了对长时处理过程的支持,这可以大大提高这些软件本身的采用率。 ^33380964-36-13063-13104

    • ⏱ 2022-06-19 12:25:18

唯一标识

  • 📌 在设计实体时,我们首先需要考虑实体的本质特征,特别是实体的唯一标识和对实体的查找,而不是一开始便关注实体的属性和行为。 ^33380964-41-522-581

    • ⏱ 2022-06-19 17:49:29
  • 📌 将唯一标识的生成委派给持久化机制 ^33380964-41-7565-7581

    • ⏱ 2022-06-21 10:10:12
  • 📌 在Product初始化完成之后,系统将产生一个领域事件,该事件将保存在事件存储(8)中。最后,所存储的事件将被外部限界上下文中的订阅方所接收。在图5.3中,在客户端将Product加到ProductRepository之前,领域事件有可能已经被订阅方接收到了。因此,此时领域事件中并不包含新建Product的实体标识。为了正确地创建领域事件,我们应该及早生成实体标识,如图5.4所示。客户端向ProductRepository获取下一个实体标识,然后将该标识作为参数传递给Product的构造函数。 ^33380964-41-13360-13644

    • ⏱ 2022-06-21 10:26:05
  • 📌 第一列,id,即为委派标识。最后一行将id作为表的主键。这里我们可以区分出委派标识和领域标识。其中的两列——tenant_id_id和username——提供了领域标识,它们联合起来形成了一个唯一键k_tenant_id_username。 ^33380964-41-16782-16931

    • ⏱ 2022-06-21 10:33:10
  • 📌 领域标识不需要作为数据库的主键。这里我们只将委派标识id作为了主键, ^33380964-41-16931-16965

    • ⏱ 2022-06-21 10:34:58
  • 📌 我们需要使用两种标识,一种为领域所使用,一种为ORM所使用,在Hibernate中,这被称为委派标识(Surrogate Identity)。 ^33380964-41-15161-15261

    • ⏱ 2022-06-21 10:35:34

发现实体及其本质特征

  • 📌 属 ^33380964-42-22601-22602
    • ⏱ 2022-06-22 09:58:48

第7章 领域服务

  • 📌 领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合(10)和值对象(6)上时,最好的方式便是使用领域服务了。有时我们倾向于使用聚合根上的静态方法来实现这些这些操作,但是在DDD中,这是一种坏味道。 ^33380964-52-525-713
    • ⏱ 2022-06-22 11:42:07

什么是领域服务(首先,什么不是领域服务)

  • 📌 当领域中的某个操作过程或转换过程不是实体或值对象的职责时,此时我们便应该将该操作放在一个单独的接口中,即领域服务。请确保该领域服务和通用语言是一致的;并且保证它是无状态的。 ^33380964-53-1540-1626
    • ⏱ 2022-06-22 11:47:29

建模领域服务

  • 📌 我们并不推荐将资源库对BacklogItem的获取放在聚合实例中,相反,将其放在领域服务中则是一种好的做法。 ^33380964-55-6743-6797
    • ⏱ 2022-06-22 12:09:11

向远程限界上下文发布领域事件

  • 📌 领域模型的持久化存储中,创建一个特殊的存储区域(比如一张数据库表),该区域用于存储领域事件。这便是一个事件存储(Event Store) ^33380964-62-1690-1758

    • ⏱ 2022-06-23 09:34:24
  • 📌 同时,你需要创建一个消息外发组件将事件存储中的所有消息通过消息机制发送出去。这种方式的优点在于:模型修改和事件提交可以同时位于单个本地事务中。另一个额外的优点是,我们可以发布基于REST的事件通知。使用这种方式时,消息机制所使用的消息存储是完全私有的。在将领域事件保存到事件存储之后,我们需要使用一个消息中间件来发送消息。因此,这种方式的缺点是,我们可能需要定制开发一个消息转发组件来发送消息,同时客户方需要对消息进行消重处理(请参考“事件存储”)。 ^33380964-62-1821-2046

    • ⏱ 2022-06-23 09:34:58

事件存储

  • 📌 对于单个限界上下文的所有领域事件来说,为它们维护一个事件存储是有好处的。 ^33380964-63-415-451
    • ⏱ 2022-07-08 09:53:43

实现

  • 📌 将订阅方/接收方设计成幂等的 ^33380964-65-10610-10624

    • ⏱ 2022-07-08 17:46:48
  • 📌 在订阅方的持久化机制中保存消息的话题/交换器名称和一个唯一的消息ID ^33380964-65-10709-10743

    • ⏱ 2022-07-08 17:48:38
  • 📌 在使用常用的消息中间件产品时,只保存最近处理的消息是不够的,因为消息的到达可能是无序的。因此,如果一个消重查询在检查那些ID小于最近一次所处理消息的ID的消息时,它有可能忽略掉一部分消息。 ^33380964-65-10902-10996

    • ⏱ 2022-07-08 17:50:47
  • 📌 在两种情况下——消息中间件订阅方和基于REST的消息客户方——我们都应该保证:对跟踪信息的修改和本地模型状态的修改必须一同提交。否则,对模型的修改和对跟踪信息的修改将无法达到一致。 ^33380964-65-11215-11305

    • ⏱ 2022-07-08 18:06:31

原则:在一致性边界之内建模真正的不变条件

  • 📌 在一个事务中只修改一个聚合实例 ^33380964-76-1542-1557

    • ⏱ 2022-07-11 11:22:16
  • 📌 这里的不变条件表示一个业务规则,该规则应该总是保持一致的。 ^33380964-76-554-583

    • ⏱ 2022-07-11 18:47:34

原则:设计小聚合

  • 📌 在聚合中,如果你认为有些被包含的部分应该建模成一个实体,此时你该怎么办呢?首先,思考一下,这个部分是否会随着时间而改变,或者该部分是否能被全部替换。如果可以全部替换,那么请将其建模成值对象,而非实体。 ^33380964-77-2144-2244
    • ⏱ 2022-07-11 11:37:02

原则:在边界之外使用最终一致性

  • 📌 有时,领域专家甚至比开发者更能接受这种延迟。因为他们能意识到,在他们的业务中,延迟是客观存在的,而开发人员则总是期待着原子性操作。 ^33380964-79-829-894

    • ⏱ 2022-07-11 18:13:58
  • 📌 在DDD中,有一种很实用的方法可以支持最终一致性,即一个聚合的命令方法所发布的领域事件及时地发送给异步的订阅方 ^33380964-79-1033-1088

    • ⏱ 2022-07-11 18:14:49
  • 📌 在接收到事件之后,每个订阅方都会获取自己的聚合实例,然后在该聚合上完成相应的操作。每个订阅方都在单独的事务中进行操作,也即满足了“在一次事务中只修改一个聚合实例”的原则。如果一个订阅方与其他客户端发生了并发竞争而使修改失败怎么办?此时,订阅方并不会向消息机制发回成功确认信号,所以消息会重发,然后开始一个新的事务重新触发更新操作。这个过程将持续进行直到一致性得到满足或者达到重试上限为止[6]。如果更新彻底失败,此时我们可以做个妥协,或者发出失败报告。 ^33380964-79-1306-1683

    • ⏱ 2022-07-11 18:18:55
  • 📌 对于一个用例,问问是否应该由执行该用例的用户来保证数据的一致性。如果是,请使用事务一致性,当然此时依然需要遵循其他聚合原则。如果需要其他用户或者系统来保证数据一致性,请使用最终一致性。 ^33380964-79-3045-3137

    • ⏱ 2022-07-11 18:26:45

实现

  • 📌 对于所依赖的对象,我们应该在聚合命令方法执行之前进行查找,然后再将其传入命令方法。 ^33380964-82-8173-8214

    • ⏱ 2022-07-24 19:47:21
  • 📌 不要在聚合中注入资源库和领域服务,而在其他多数情况下,依赖注入都是很适合的。比如,我们可以向应用服务中注入资源库和领域服务。 ^33380964-82-8502-8564

    • ⏱ 2022-07-24 19:50:28

领域服务中的工厂

  • 📌 UserInRoleAdapter只负责与外部上下文的通信,而CollaboratorTranslator则只负责翻译和创建新实例。 ^33380964-87-3180-3246
    • ⏱ 2022-08-05 10:02:20

面向集合资源库

  • 📌 一个资源库应该模拟一个Set集合。 ^33380964-90-2539-2556

    • ⏱ 2022-08-05 10:17:07
  • 📌 隐式读时复制 ^33380964-90-4046-4052

    • ⏱ 2022-08-05 10:21:23
  • 📌 隐式写时复制 ^33380964-90-4340-4346

    • ⏱ 2022-08-05 10:23:48
  • 📌 持久化机制必须能够隐式地跟踪发生在每个持久化对象上的改变。有多种方法都可以达到这样的目的,包括: ^33380964-90-3966-4014

    • ⏱ 2022-08-05 10:26:39

面向持久化资源库

  • 📌 面向持久化资源库 ^33380964-91-479-487

    • ⏱ 2022-08-05 10:55:13
  • 📌 基于保存操作的资源库 ^33380964-91-492-502

    • ⏱ 2022-08-05 10:55:32
  • 📌 最大的不同在于客户端对这些方法的使用。在使用面向集合风格时,聚合实例只有在新创建的时候才会使用add()或addAll()方法;然而在使用面向持久化风格时,无论是创建聚合还是修改聚合,我们都必须使用save()或saveAll()方法 ^33380964-91-3496-3613

    • ⏱ 2022-08-05 11:04:26

管理事务

  • 📌 通常来说,我们将事务放在应用层(14)中 ^33380964-93-666-839
    • ⏱ 2022-08-05 17:36:25

资源库 vs 数据访问对象(DAO)

  • 📌 有时,资源库和数据访问对象——即DAO——被当作同义词看待。 ^33380964-95-429-459

    • ⏱ 2022-08-05 17:50:03
  • 📌 资源库和DAO是不同的。一个DAO主要从数据库表的角度来看待问题,并且提供CRUD操作。 ^33380964-95-589-633

    • ⏱ 2022-08-05 17:50:09
  • 📌 而另一方面,资源库和数据映射器(Data Mapper)则更加偏向于对象,因此通常被用于领域模型中。 ^33380964-95-942-1027

    • ⏱ 2022-08-05 17:50:14
  • 📌 在DAO模式中所执行的CRUD操作都是可以放在聚合中来实现的,因此,我们应该尽量避免在领域模型中使用这些DAO模式。 ^33380964-95-1056-1114

    • ⏱ 2022-08-05 17:50:50
  • 📌 通常来说,你可以将资源库当作DAO来看待。但是请注意一点,在设计资源库时,我们应该采用面向集合的方式,而不是面向数据访问的方式。这有助于你将自己的领域当作模型来看待,而不是CRUD操作。 ^33380964-95-1577-1670

    • ⏱ 2022-08-05 17:53:50

测试资源库

  • 📌 我们可以从两个方面对资源库进行测试。首先,我们需要测试资源库本身是能正确工作的。其次,我们还要测试对资源库的使用,以保证能够正确地保存和获取聚合实例。对于前者,我们必须使用产品环境下的资源库实现。对于后者,我们既可以使用产品实现,也可以使用内存实现。 ^33380964-96-416-541
    • ⏱ 2022-08-05 17:55:15

通过REST资源集成限界上下文

  • 📌 请注意,这里的TranslatingCollaboratorService位于基础设施层的某个模块(9)中。虽然我们将独立接口CollaboratorService当作领域模型的一部分,并将它放置在了六边形的内部,但是它的实现却是技术性的,并且被放置在了六边形架构的外部,即端口和适配器所在的位置。作为技术实现的一部分,在防腐层中通常会有一个特定的适配器[Gamma et al.]和翻译器。再回头看看图13.1,你将看到其中的适配器UserInRoleAdapter和翻译器CollaboratorTranslator。这个特定的UserInRoleAdapter负责与远程系统的交互以请求所需的User-Role资源: ^33380964-100-6723-7133

    • ⏱ 2022-08-05 20:48:03
  • 📌 如果GET请求得到了成功(状态码200)的应答,表明该UserInRoleAdapter获取到了相应的User-Role资源。之后,CollaboratorTranslator负责将该资源翻译成Collaborator的子类对象: ^33380964-100-7351-7466

    • ⏱ 2022-08-05 20:48:11

用户界面

  • 📌 一种渲染多个聚合实例的方法便是使用数据传输对象(Data Tranfer Object,DTO) ^33380964-104-1877-1960

    • ⏱ 2022-08-06 00:05:13
  • 📌 DTO将包含需要显示的所有属性值。 ^33380964-104-1976-1993

    • ⏱ 2022-08-06 00:05:31
  • 📌 它的缺点在于,我们需要创建一些与领域对象非常相似的类。 ^33380964-104-2562-2589

    • ⏱ 2022-08-06 00:05:43
  • 📌 可以将展现模型看成是一种适配器 ^33380964-104-8713-8763

    • ⏱ 2022-08-06 00:20:16

应用服务

  • 📌 我们应该将所有的业务领域逻辑放在领域模型中,不管是聚合、值对象或者领域服务;而将应用服务做成很薄的一层,并且只使用它们来协调对模型的任务操作。 ^33380964-105-916-987

    • ⏱ 2022-08-06 00:24:57
  • 📌 命令对象即“将一个请求封装到一个对象中, ^33380964-105-5242-5262

    • ⏱ 2022-08-06 00:31:58

基础设施

  • 📌 基础设施的职责是为应用程序的其他部分提供技术支持。 ^33380964-107-416-441

    • ⏱ 2022-08-06 00:39:24
  • 📌 在应用服务获取资源库时,它只会依赖于领域模型中的接口,而实际使用的则是基础设施中的实现类。 ^33380964-107-596-641

    • ⏱ 2022-08-06 00:40:47

读书笔记

本书评论