《实现领域驱动设计》读书笔记

引:《领域驱动设计》为我们提供各种方式来更好的建模,以及更高视角来对待系统模型,同时还提供一组领域建模词汇库。但是关于如何使用,还差了点点点意思,而《实现领域驱动设计》更能让我们付诸实践。本书从顶向下,从战略到战术用一个虚构的案例贯穿全书。冲冲冲!

DDD入门

介绍DDD的好处,以及DDD的使用,用一个例子开篇

什么是领域模型?: 领域模型是关于某个特定领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且表达了准确的业务含义。

初级开发者:按照DDD去组织一部分代码

中级开发者:按照DDD去组织一个项目

高级开发者:按照DDD去组织一个系统

领域专家:按照DDD去沟通

项目经理:按照DDD去组织一个系统(包括技术和业务)

贫血对象的失忆症: Java Bean的get/set方法让人不知道业务属性,从而忘了该方法创建出来的作用。

如何DDD: 如果不谈实现细节,那么DDD最重要的东西就是通用语言和界限上下文。界限上下文是整个应用程序之内的一个概念性边界,这个边界之内的每种领域术语、词组和句子(通用语言),都有确定的上下文含义。

如何判断掌握通用语言

  • 同时绘制物理模型图和概念模型图,并标以名字和行为
  • 创建一个包含简单定义的术语表
  • 让团队检查成果

DDD的业务价值

  1. 获得了一个非常有用的领域模型
  2. 业务得到了更准确的定义和理解
  3. 领域专家可以为软件设计做出贡献
  4. 更好的用户体验
  5. 清晰的模型边界
  6. 更好的企业架构
  7. 敏捷、迭代式和持续建模
  8. 使用战略和战术新工具

实施DDD所面临的挑战:

  • 为创建通用语言腾出时间和精力
  • 持续地将领域专家引入项目
  • 改变改开发者对领域的思考方式

哈哈哈 例子有点意思,从小拉大,以小应用作为突破点,给整体带来增长

领域、子域和界限上下文

战略设计:用界限上下文分离出子域和核心域

领域: 即是一个组织所做的事情以及其中所包含的一切。当时一个领域太大了,我们需要划分为若干子域,领域模型在界限上下文中开发。在开发一个领域模型时,我们关注的通常只是这个业务系统的某个子域。

对于一个零售商来说,主要分为4个主要的子域:产品目录,订单,发票和物流,库存

虚线表示不同的子域、实线表示界限上下文,不同子域和上下文之间的直线表示集成关系。

在DDD中,简单的子域可以以模块的形式从核心域分离出来

在一个好的界限上下文中,每个术语应该仅表示的一种领域概念,例如订单子域和产品目录子域对于顾客的含义是不一样的。

一个业务系统领域包含:核心域(业务成功的主要促成因素)、支撑子域、通用子域(被应用于整个业务系统)

协作模型中真正重要的是角色,而不是所谓的权限,要区分出一个对象在不同上下文中概念的不同,然后进行隔离

在一个系统中,子域和界限上下文之间很难存在一对一的映射关系

一个界限上下文不只包含领域模型,还包含了一种业务服务。

如果数据库和领域模型不能完全对应,就要区分出两个不同的上下文。

TODO:画一个上下文

一个团队,一个界限上下文

SasSOvation的拆分:敏捷项目管理(核心域);协作(支撑);身份与访问(通用,一定要区分开来)

协作上下文:论坛、共享日历、博客、即时消息、wiki、留言板、文档管理、通知与提醒、活动跟踪和RSS订阅

问题空间: 我们思考的是业务所面临的挑战,对问题的空间的开发将产生一个新的核心域,对问题空间的评估应该同时考虑已有的子域和额外所需子域。因此问题空间是核心域和其他子域的组合。

解决方案:我们思考的是如何实现软件以解决这些业务挑战,解决方案空间包括一个或多个界限上下文,即一组特定的软件模型。

上下文映射图

上下文映射图帮助我们理解业务领域、模型间的边界,以及这些边界之间的集成方式。

集成关系用上游和下游表示(上游的改变会影响下游)

通过开放主机这种上下文关系区分出 协作上下文和身份和访问上下文,开发主机通过固定的协议与其他服务通信,这个协议可以包括(XML、JSON、Protocol Buffer、Thrift、消息(发布语言))等等。

通过反腐层保护住协作上下文和敏捷项目管理上下文,下游的反腐层将返回内容翻译成本地上下文的领域对象

为了实现自治,敏捷项目管理上下文可能将保留本地模型(使用值对象),并通过事件的方式或者周期调用URL进行同步

总结:

对于一个上下文需要考虑到底要不要自治

如果是自治,那就会存上游服务的内容,是一个值对象

如果不是自治,那么这边就不会存上游服务的内容,用ID表示

架构

各种架构 用例驱动架构

分层架构中使用依赖导致原则(整洁架构),可以在测试中轻易替换UI层和基础设施层

分层架构

用户界面层 -》 应用层-》 领域层 -》 基础设施层

原则:每层只能与位于其下方的层发生耦合,事实上,较低层也是可以和较高层发送耦合的,但这只局限与观察者模式或中介者模式????

用户界面层:用户界面只用于处理用户系那是和用户请求,他不应该包含领域或业务逻辑,这里不包括对领域模型的验证,如果用户界面层使用了领域模型中的对象,那么此时的领域对象仅限于数据的渲染展示。这个时候,可以采用展现模型(Presentation Model,可能我们更喜欢叫View Object VO)。由于业务可能是人,也可能是其他系统,有时用户界面层将采用开发主机服务的方式向外提供API。

应用层: 应用服务位于应用层(也可以叫Usercase层)应用服务可以用于控制化事务和安全认证,或者向其他系统发送基于事件的消息通知。应用服务本身并不处理业务逻辑(这里特指领域服务)。他是领域模型的直接客户。应用服务

应该给是很轻量的,他主要用于协调对领域对象的操作,比如聚合??。同时应用服务是表达用例和用户故事的主要手段。因此,应用服务的通常用途是:接受来自用户界面的输入参数,再通过资源库获取到聚合实例,然后执行相应的命令操作。

基础设施层:在传统的分层架构中,将基础架构层放在最底层是有确定的,因为他适合写单元测试。一般会通过依赖倒置让基础设施层依赖高层模块。依赖倒置一般通过依赖注入来实现,而依赖注入一般交由依赖注入框架实现。

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

分为外部区域(各种适配器,这里可以是HTTP、RPC、MQ的输入或者是各种存储的输出)和内部区域(用例以及领域模型)

其实和整洁架构很像,哈哈哈哈

面向服务架构

服务设计原则:

  1. 服务契约:通过锲约文档,服务阐述自身的目的与功能
  2. 松耦合:服务将依赖关系最小化
  3. 服务抽象:服务只发布契约,而向客户隐藏内部逻辑
  4. 服务可重用性:一种服务可以被其他服务所重用
  5. 服务自治性:服务自行控制环境与资源以保持独立性???感觉上肯定会有各种依赖
  6. 服务无状态性:服务负责消费方的状态管理???
  7. 服务可发现性:客户可以通过服务元数据来查找服务和理解服务
  8. 服务组合性:一种服务可以由其他的服务组合而成,而不管其他服务的大小和复杂性如何

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

当我们需要从不同的Repository获取聚合实例数据的时候,我们将需要将这些实例数据组成成一个数据传输对象(Data Transfer Object, DTO)

CQRS是将紧缩对象(或组件)设计原则和命令——查询(CQS)应用在架构模式中的结果,具体解释是:一个方法要么是执行某种动作的命令,要么是返回数据的查询,而不能两者皆是。在对象层面,这意味着:

  1. 如果一个方法修改了对象的状态,该方法便是一个命令(Command),他不应该返回数据。但是好像还是一般返回ID???(对应命令处理器),一个命令执行执行结束可能发出领域事件,这个时候可能有另外一个实例同步更新,这个时候需要保持最终一致性
  2. 如果一个方法返回了数据,该方法便是一个查询(Query),此时他不应该通过直接或者间接的手段修改对象的状态。(对应查询模型,读库,一种是利用数据库自带的同步机制,另外一种是通过领域事件(事件源)来更新查询模型,还有一种是通过ETL将数据经过转化加载到查询模型)

这样好像能够读写分离,但是现实中还是会有写后读的场景。

大部分命令模型和查询模型之间的不同步并不是什么大问题,但是我们也可以通过其他方式来解决,比如通过引入观察者模式(??)或者分布式缓存(这个可以理解)

如果用户界面并不过去复杂,或者我们只需要在当个视图中处理聚合,那么引入CQRS反而会增加额外的复杂性(??不太懂,感觉都挺好的样子)

事件驱动架构

事件驱动架构是一种用于处理事件的生成、发现和处理等任务的软件架构。

该架构可以通过消息机制完成对所有系统的解耦。

对于不同的界限上下文来说,不同的领域事件具有不同的含义,也有可能没有任何含义(如果使用消息过滤器或者路由关键字,消息订阅方可以避免接收对自己无意义的消息)

管道和处理器

下面是基于消息的管道和过滤器处理过程的基本特征

  1. 管道是消息通道:过滤器通过输入管道接收数据,过道输出管道发送数据
  2. 端口连接过滤器和管道:过滤器通过端口连接到输入和输出管道。端口使得六边形架构成为首选的架构
  3. 过滤器即是处理器:过滤器可以对消息进行处理,而不见得一定对消息进行过滤
  4. 分离处理器:每个过滤处理器都是一个分离的组件
  5. 松耦合:每个过滤器都是相对独立的参与处理过程,处理器组合可以通过配置完成
  6. 可换性:更具用例需要,我们可以重新组织不同处理器的处理顺序,这同样是通过配置完成
  7. 过滤器可以使用多个管道:消息过滤器可以从不同的管道中读写数据
  8. 并行使用同种类型的过滤器:对于最繁忙的和最慢的过滤器来说,我们可以并行地采用多个相同类型的过滤器来增加处理量

长时处理过程(Saga)

在某个消息发出之后,走入不同的管道,最后回到同一个处理器

这里有执行器(并行),最后的处理器(状态跟踪器),如果超时,则再发出一个取消事件(这是完成一致性)

感觉这是一个分布式事务实现方案,但是书里却说和分布式事务没有什么关系(不懂???)

事件源

为了了解实体的变更,每个领域事件都将被保存到事件存储,然后如果要读取,则根据发生在改聚合上的历史事件来重建改聚合实例,事件的作用顺序应该与他们的产出顺序相同(有序消息

为了避免聚合事件操作对于业务操作繁忙的模型产生影响,我们可以通过聚合状态的快照的方式进行优化。快照并不是随意创建的,而是可以在所发生的时间达到某个数量时才创建的。

下面是事件源技术所带来的业务优势:

  1. 用新的或者修改后的时间存储打补丁可以修正许多问题。
  2. 我们可以通过重放一组事件的方式来重做或测校对模型的修改、
  3. 有了所有事件的历史信息,并基于此可以做很多事

缓存

通过事件重建缓存,然后走缓存来实现查询模型

实体

实体是一个唯一的东西,并且可以在相当长的一段时间内持续的变化。

在设计实体时,我们首先需要考虑实体的本质特征,特别是实体的唯一标识和对实体的查找,而不是一开始变关注实体的属性和行为。只有在对实体的本质特征有用的情况下,才加入相应的属性和行为。

一些常用的创建实体身份标识的策略,从简单到复杂依次为:

  1. 用户提供一个或多个初始唯一值作为程序输入,程序应该保证这些初始值的唯一 (比如用户创建一个用户名)
  2. 程序内部通过某种算法自动生成身份识别,此时可以使用一些类库或框架,(UUID/snowflake雪花算法)
  3. 程序依赖持久化存储,比如数据库来生成唯一标识(自增ID)
  4. 另一个界限上下文(系统)已经决定出一个唯一标识,这作为程序的输入,用户可以在一组标识进行选择(比如邮箱、手机号)

为了发布领域事件,我们需要提前发布领域事件(又让我想起了某个项目的某个坑)

通过分析用例(或者说需求),提炼出实体、实体行为、值对象和领域服务

所谓的Setter方法也要变成带有领域行为色彩的方法名

建模的一个方面便是发现对象的角色和职责。通常来说,对角色和职责分析是可以应用在领域对象上的。

角色就是接口???一个对象可以有多个角色,对应不同的职责活动

验证:验证的主要目的在于检查模型的正确性、检查的对象可以是某个属性(采用自封装性),也可以是整个对象(使用规范或者策略(单独的验证类)来验证,延迟验证),甚至是多个对象的组合。

跟踪变化:实体是可以变化的,在整个生命周期中,对于领域专家来说可能会更关心发生在模型中一些重要的事件,在实际的实现上,最实用的方式就是领域事件和时间存储了,文中说订阅方将事件保存在实践存储中(??)

值对象

值对象的优点:值对象用于度量和描述事务,我们可以非常容易的对值对象进行创建、测试、使用、优化和维护。

我们应该尽量使用值对象来建模而不是实体对象。

当你只关心某个对象的属性是,该对象便可以成为一个值对象。为其添加有意义的属性,并赋予他相应的行为。我们需要将值对象看出不变对象,不要给他任何身份标识、还应该避免想实体对象一样的复杂性。

当你决定一个领域概念是否是一个值对象时,你需要考虑是否拥有以下特征:

  1. 度量或者描述领域中的一件东西
  2. 他可以作为不变量(只暴露初始化方法)
  3. 他将不同的相关的属性组合成一个概念整体(概念必须是整体的)
  4. 当度量和描述改变时,可以用另一个值对象予以替换 (是替换(a=3 => a = 4),不是修改(a= 3 =》 a = a + 1))
  5. 他可以和其他值对象进行相等性比较(属性都相等,那么这个值对象就是相等的)
  6. 他不会对协作对象造成副作用(不修改对象的状态)

在函数式编程中:只允许无副作用的行为存在,并且要求所有闭包只能接受和产生不变的值对象

最小化继承:在模型概念从上游上下文流入到下游上下文中,尽量使用值对象来表示这些概念。使用不变的值对象是的我们做更少的职责假设。

用值对象表示标准类型:就是类型的定义使用值对象,枚举;我们需要每组标准类型创建一个领域服务或工厂。

测试值对象行为测试可以驱动对领域模型的设计。所以测试也应该具有领域含义。

实现值对象:拥有两个构造函数,一个是所有属性的构造函数,一个是拷贝构造函数(用于验证不变形)

持久化值对象:不应该使持久化机制影响到对值对象的建模,根据领域模型来设计数据模型,而不是根据数据模型来设计领域模型。(有时候可以将值对象放在实体一起进行数据库建模,对于List,可以json化后作为一列,这自然违反了数据库建模范式,但是好处是明显的,缺点是:数据库的列宽,必须查询,需要自定义类型;;;或者采用层超模型,隐藏值对象在数据库中的ID)

领域服务

领域中的服务表示一个无状态的操作,他用于实现特定在某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务。

尽量避免在聚合中使用Repository

应用服务不关心业务逻辑、领域服务处理业务逻辑

我们可以使用领域服务进行下面的操作:

  1. 执行一个显著的业务操作过程(需要多个聚合的输入)
  2. 对领域对象进行转换
  3. 以多个领域对象作为输入进行计算,结果产生一个值对象 (比如计算统计结果??)

比如认证就可以放在领域服务中 (需不需要独立的接口,看是否有多个实现)

在应用服务层关心事务

领域事件

聚合本身将发布领域事件

使用领域事件来捕获发送在领域中的一些事情。领域事件时一个功能强大的建模工具,一旦使用了他,你便爱不释手(哈哈哈,解耦太舒服了),但是在你开始使用领域事件时,你要做的是对不同的事件进行定义

领域事件: 领域专家锁关系的发生在领域中的一些事件。将领域中所发生的活动建模成一系列的离散事件,每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。

注意听下面的话语:

  1. 当…..
  2. 如果发生….
  3. 当….的时候,请通知我 (发出事件)
  4. 发生….时

当领域事件到达目的地之后,无论是本地系统还是外部系统,我们通常将领域事件用于维护事件的一致性,这是有意而为之的,这样可以消除两阶段提交(全局事务),还可以支持聚合原则。聚合的其中一个原则是,只允许对一个聚合实例进行修改,由此产生的其他改变必须在单独的事务中完成(所以应该将事务放在应用服务层吗??)。因此,本地界限上下文中的其他聚合实例便可以通过领域事件的方式予以同步。另外,领域事件还可以用于使远程依赖系统与本地系统保持一致。本地系统和远程系统的解耦有助于提高双方协作服务的可伸缩性。

对于批处理处理,可以利用领域事件,将一个大事务拆分成较小的处理单元。

命名:我们应该根据界限上下文中的通用语言来命名事件及其属性。如果事件由聚合上的命令操作产生,那么我们通常根据该操作方法的名字来命名领域事件。

组成:可以有一个公共的父类用于保证一些基本的信息,然后属性基本是各个ID;TenantID在Saas应用中是必须的

存在的方式: 有时,领域事件并不由聚合中的命令方法产生,而是直接由客户方所放出的请求产生,此时,领域事件可以建模成一个聚合(落库),并且可以拥有自己的资源库。他也应该有自己的唯一表示(因为没有ID,不能判断是否是重复事件),客户方可以通过调用领域服务来创建事件,然后将其添加到资源库。然后通过发送事件。

发送:在聚合汇总添加一个简单的服务,该服务用于通知订阅费所发生的的领域事件,如果是在当个线程内,可以通过ThreadLocal来实现发布-订阅,可能在聚合中发出事件,也有可能在领域服务中发出

订阅: 通常来说,注册订阅方这种功能由应用服务完成,有时也有领域服务完成。

消息设施的一致性:有下面三种基本的方式:

  1. 领域模型和消息设施共享持久化存储
  2. 领域模型的持久化存储和消息的持久化存储由全局的XA事务(两阶段提交)
  3. 在领域模型的持久化存储中,创建一个特殊的存储区域(比如一张数据库表,该区域用于存储领域事件),然后创建一个消息外发组件(可以利用binlog)将时间存储中的所有消息通过消息机制发送出去(这种方式很常用)

通过异步的消息,服务可能达到更高层次——自治性,但是有些团队只提供RPC的方式

事件存储:对于单个界限上下文的所有领域事件来说,为他们维护一个事件存储是有好处的 (订阅方存储事件也是有好处的)

实现: 可以参考书本,利用RocketMQ + TimberMBean 或者 Quartz

事件去重: 这个就很常见啦,我们要保持幂等消费,一种方式就是通过持有久化存储事件,在每次消费之前都查看事件是否已经被消费

领域事件集成的两种方式:1. 基于REST的消息通知;2. 消息中间件

模块

可以看场的Java的命名空间,包 import

在DDD中,模型中的模块表示了一个命名的容器,用于存放领域中内聚在一起的类。将类放在不同模块中的目的在于达到松耦合性。下面是模块的设计原则:

  1. 模块应该和领域概念保持协调一致:通常对于一个或一组内聚的聚合来说,我们都相应地创建一个模块。
  2. 根据通用语言来命名模块:这也是DDD的基本目标
  3. 不要机械式的根据通用的组件类型和模式来创建模块:就是不要把领域放在一个模块,把工厂放在一个模块
  4. 设计松耦合的模块:模块间的松耦合性与类间的松耦合性具有相同的好处。
  5. 当同层模块间出现耦合时,我们应该杜绝循环依赖:比如产品依赖开发团队,但是开发团队不依赖于产品
  6. 在父模块和子模块之间放松原则:尽量避免
  7. 不要将模块设计成一个静态的概念,而是与模型中的对象一道进行建模

命名: 上下文 + 分层 + 模型 + 模块

领域服务可用来当防腐层

先考虑模块,再考虑界限上下文

如果没有明确的上下文边界,使用模块即可

聚合

这里特别关注事务处理保证最终一致性;淡然这些都是建立在用例之上的

聚合的粒度是需要关注的,因为他会影响性能,比如为了并发安全,采用多个聚合时候的时候,原来的最大的聚合更多时候变成了工厂。

一致性原则往往是聚合边界条件,为了保持一致性,意味着客户请求应该只在一个聚合实例上执行一个命令方法

设计小聚合往往是因为要考虑性能,可以减少失误的提交冲突

原则:通过唯一标识引用其他聚合 对于在单个事务更新多个值对象的情况,可能应该通过最终一致性来保证,但是这种可能有性能问题,但是好处是增强模型的可伸缩性

原则:在边界之外使用最终一致性 只要延迟可以接受 通过领域事件实现

指导原则: 对于一个用例,问问是否应该由执行该用例的用户来保证数据的一致性。如果是,请使用事务一致性,当然此时已然遵循其他聚合原则,如果需要其他用户或者系统来保证数据一致性,请使用最终一致性。

打破原则的理由 有时我们确实会选择在单个事务中更新多个聚合实例,下面是一些理由:

  1. 方便用户界面:对于有公共属性的多个对象
  2. 缺乏技术机制:没有消息,定时器或者后台线程之类的技术
  3. 全局事务:使用全局事务,使系统很难有好的伸缩性
  4. 查询性能

在分析具体的聚合根的时候,可以考虑将XXID作为值对象显示描述出来

实现: 使用迪米特法则“告诉而非询问”原则

迪米特法则(最小知识原则):任何对象的任何方法只能调用以下对象的方法,该对象自身,它所创建的对象、自身所包含的对象、所传入的对象

告诉而非询问原则: 客户端对象不应该首先询问服务对象,然后根据询问结构,调用服务对象中的方法,而是应该通过调用服务对象的公共接口的方式来告诉服务对象所要执行的原则。

尽量不要在聚合中注入资源库和领域服务,我们可以向应用功能服务注册如资源库和领域服务

工厂

使用工厂的主要动机: 将创建复杂对象和聚合的职责分配给一个单独的对象,改对象本身并不承担模型中的职责。工厂应该提供一个创建对象的接口,改接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去应用那个实际被创建的对象。对于聚合来说,我们应该一次性创建整个聚合,并且确保他的不变条件得到满足。

领域服务有时候也会扮演工厂的角色(在领域服务中调用防腐层),或者使用聚合的工厂方法(会隐藏??)。如果有一个类层级的对象,我们可以考虑使用抽象工厂模式来创建不同类型的对象。

资源库

有两种类型的资源库设计:面向集合的设计 和 面向持久化的设计 (这两种风格我暂时分不出来)推荐面向持久化

面向集合资源库:使用集合的方法 add, addAll,remove,removeAll,size

精要:一个资源库应该模拟一个Set集合,无论采用什么类型的持久化机制,我们都不应该允许多次添加一个聚合实例,另外,当从资源库获取到一个对象并对其进行修改时,我们并不需要”重新保存”该对象到资源库中。关于删除,需要注意逻辑删除。

面向持久化资源库: 这是一种基于保存操作(Save)的资源库

精要:在向数据库存储中添加新建对象或修改既有对象时,我们都必须显示地调用put方法,该方法将以新的值来替换先前关联在某个键上的原子。这种类型的存储可以极大简化对聚合的读写。????模拟的是一个Map

对于资源库的不同实现,基础设施的依赖不一样。

下面是为资源库接口添加额外行为的原因:

如果我们需要获取聚合根下的某些子聚合:我们不用先从资源库中获取聚合根,然后再从聚合根中获取这些子聚合,而是可以直接从资源库中返回

如果我们需要在用户界面上显示数据,而这些数据来自于多个聚合,此时我们不用先分别获取到每个聚合,再从中提取出所需数据,而是可以使用用例优化查询的方法直接查询所需数据,然后将查询结果放在一个值对象返回

管理事务:对事务的管理绝对不应该放在领域模型和领域层中,通常来说,我们将事务放在应用层中。然后为每个主要的用例创建一个门面(保证事务方法),我们一般会采用声明事务。Spring使用声明式事务真方面,有空可以看看它的实现。

资源库和数据访问对象(DAO)的区别:一个DAO主要从数据库表的角度来看待问题,更适合事务脚本程序。一个Repository更加偏向于对象,被用于领域模型中

测试资源库: 首先,我们需要测试资源库本身是能正常工作的,其次还要测试对资源库的使用,以保障能够正确的保存和获取聚合实例(使用内存)。

集成界限上下文

上下文映射图存在的两种主要形式,一种是通过绘制一些简单的框图来展示他们之间的集成关系;另外一种则是通过代码来实现这些集成关系。

集成方式 RPC or 消息 or HTTP REST

  1. RPC :一种集成方式就是在一个界限上下文中暴露应用程序接口(API),然后在另一个界限上下文中通过RPC的方式访问该API。在RPC需要考虑自治。

  2. 消息:通过消息队列进行发布-订阅,这个方式能获得更高层次的自治性。事件命名(Topic)应该是模块+事件名;对于事件消费的第一步必然是对事件的过滤,有些时候需要保证消息的有序。要控制上下文事件的边界,尤其是对长时处理系统(可以再看看)

  3. HTTP REST: 和RPC不同的是,通过URI来表示资源

分布式计算原则

  1. 网络是不可靠的
  2. 总是存在时间延迟,有时非常严重
  3. 带宽是有限的
  4. 不要假设网络是安全的
  5. 网络拓扑结构将发生变化
  6. 知识和政策在多个管理员之间传播???
  7. 网络传输是有成本的
  8. 网络是异构的

共享内核: 是指可以直连数据库???

开放主机服务: 当一个界限上下文以URI的方式提供了大量的REST资源,然后得到一个值对象

防腐层: Service、Adapter、Translator、Facade(HTTP Client) (这个可以再理解理解,区别不同的子类)

感觉上,资源库也是一种防腐层的实现

应用程序/系统/提供多个技术服务端口的业务服务

单个界限上下文的设计

应用程序: 表示哪些支持”核心域”模型的组件,通常包括领域模型本身、用户界面、内部使用的应用服务和基础设施组件等。

用户界面: Web用户界面、小程序、本地GUI

  1. 渲染领域对象:用户界面可能需要渲染来自多个聚合实例的属性数据,但是在提交修改时,却只能一次修改一个实例
  2. 渲染数据传输对象:一个渲染多个聚合实例的方法就是使用数据传输对象(DTO),DTO将包含需要显示的所有属性值。应用服务通过资源库读取所需的聚合实例。然后使用一个DTO组装器(DTO Assemble)将需要显示的属性值映射到DTO中。

用例优化资源库查询: 利用资源库返回值对象。而不是聚合

处理不同类型的客户端: 通过在用例服务中插入一个数据转换器

基础设施: 框架也算一种基础设施(我之前的理解好像有点错??比如HibenateXXXRepository)

对于资源库的查找,可以通过依赖注入或者服务工厂隐式完成。

聚合和事件源

事件源(Event Sourcing) 通过事件来表示一个聚合的完整状态,这里的事件时自聚合创建以来的一系列事件。通过按照产生时的顺序重放这些事件,我们可以重建聚合的状态。

使用事件源维护聚合状态的方式称为A+ES(Aggregate + Event Source)

优势:

  1. 事件源确保每次聚合改变的原因都不会丢失。这种方式是具有长远的优势:可靠性、短期/长期商业智能化、数据分析、全日志记录和调试等。
  2. 只追加(append-only)特性使事件源具有很高的性能(??),并且支持不同对的数据复制方案。
  3. 这种以事件为中心的聚合设计方式使得开发者将关注点集中于通用语言所表达的行为上,因此此时并不存在ORM的阻抗失配。(但是感觉这样查询麻烦多了)

缺点:

  1. 为A+ES设计事件需要我们对业务领域具有很深的了解
  2. 实现A+ES几乎必然得需要某种形式的命令-查询职责分离,我们很难对事件流进行查询(使用读模型投射)

Just Do It