DDD领域驱动设计

为什么我们需要DDD

  • 领域专家与开发者一起工作,准确传达业务规则
  • 业务产品文档难以系统描述技术抽象实现
  • 设计就是代码、代码就是设计。设计是关于软件如何工作的,最好的编码设计来自于多次试验。
  • DDD同时提供了战略设计和战术设计两种方式,战略设计帮助我们分析哪些投入是最重要的,哪些软件资产是可以重新拿来使用的。战术设计则是帮助我们构建DDD模型中各个部件

d当我们在复杂性问题上犯错时,我们很难轻易地扭转颓势。这意味着我们应该在项目早期计划便对简单性和复杂性作出判断,这将为我们节约很多时间和开销,并免除很多麻烦。一旦我们做出了重要的架构决策、并且已经在该架构下进行了深入的开发,通常我们也被绑定在这个架构下了,所以再决定时一定要慎重。

通用语言和限界上下文同时构成了DDD的两大支柱,并且是相辅相成。

如何使用通用语言?

使用DDD的业务价值

领域、子域和限界上下文

  • 领域Domain即使一个组织所做的事情以及其中所包含的一切。在DDD中,一个领域被分为若干个子域,领域模型在限界上下文完成开发
  • 使用限界上下文和上下文映射图这样的工具可以帮助我们分析出那些概念的确属于核心域。

上下文映射图

表现的是项目当前的状态,如果项目会在将来发生变化,你可以到那时才对上下文映射图做出相应的更新。

  • 任意限界上下文关系
    • 合作关系:两个限界上下文要么一起成功要么一起失败,协调,并且在接口的演化上进行合作以同时满足两个系统的需求
    • 共享内核:对模型、代码共享产生一种紧密的依赖性,对于设计来说,这种依赖型可好可坏。我们应该对共享的部分模型指定边界,并保持共享内核小型化。在没有与另一个团队协商的情况下,这种状态是不能改变的。应该引入一种持续及成果称并保证共享内核与通用语言的一致性
    • 客户方-供应方: 当处于上下游关系中,上游可能独立于下游开发,此时应该在上游的开发计划中,顾及下游的需求
    • 遵奉者:存在上下游关系的时候尽管上游保持种种承诺,如果上游已经没有动力提供下游所需,很大可能发生承诺无法实现
    • 防腐层:集成两个设计限界上下文时,如果其余关系无法满足或者实现,那么就只能单独做一个翻译层。通过已有接口与其他系统交互,但在其内部,需要在自己模型和他方模型之间进行翻译转换
    • 开放主机服务:定义一种协议,让你的子系统通过该协议来访问你的服务,你需要将协议公开,这样任何想与你集成的人都可以使用该协议。在新的集成需求,你应该对协议进行改进或扩展,对于一些特殊需求,你可以采用一次性翻译予以处理,这样可以保持协议的简单性和连贯性。 RPC或者消息机制
    • 发布语言:在两个限界上下文之间翻译模型需要一种公用的语言,此时你应该使用一种发布出来的共享与语言来完成集成交流,发布语言通常与开放主机服务一起使用
    • 另谋他路: 在确定需求时,我们应该做得到坚决彻底,如果两套功能没有显著的关系,那么他们是可以完全被解耦的,集成总是昂贵的,有时带给你的好处也不大,声明两个限界上下文之间不存在任何关系,这样使得开发者去另外寻找简单的,专门的方法来解决问题
    • 大泥球:当我们检查已有系统时,经常会发现系统中存在混杂在一起的模型,他们之间的边界是非常模糊的,此时应该为整个系统绘制一个边界,然后将其归纳在大泥球范围之列,在这个边界之内,不要试图使用复杂的建模手段来化解问题。同时这样的系统有可能会像其他系统蔓延,应该对此保持警觉

在使用领域事件和事件驱动架构时,我们应该仔细思考最终一致性。事件不一定由消费者最终消费完成

架构

在选择架构风格和架构模式时,我们应该将软件质量考虑在内,而同时,避免滥用架构风格和架构模式也是重要的,质量驱动的架构选择是种风险驱动方式,即我们采用的架构时用来减少失败风险的。

分层

分层架构模式被认为是所有架构的始祖。它支持N层架构系统,在这种架构中,我们将一个应用程序或者系统分为不同的层次。传统的分层架构:

分层架构之间一个重要原则:每层只能与位于其下方的层发生耦合,严格分层架构:某层只能与其直接位于其下方的层发生耦合,松散分层则允许任意上方层与任意下方层发生耦合。

应用服务位于应用层中,应用服务和领域服务是不同的,因此领域逻辑也不应该出现在应用服务中,应用服务可以用于控制持久化事务和安全认证,或者向其他系统发送基于事件的消息通知。应用服务本身并不处理业务逻辑,但它却是领域模型的直接客户,应用服务是很轻量的,用于协调对领域对象的操作,比如聚合,或者接入来自用户界面的输入参数,再通过资源库获取聚合实例,然后执行相应的命令操作

依赖倒置原则

有一种方法改进分层架构--依赖倒置原则,他通过改变同之间的依赖关系达到改进目的,依赖倒置原则:高层模块不应该依赖于底层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,而细节应该依赖于抽象

六边形架构/端口与适配器

在选择持久化机制之前,我们可以在测试中采用内存资源库来模拟持久化,更多内存持久化细节,请参考资源库

面向服务架构

服务设计原则

  • 服务契约: 通过契约文档,服务阐述自身目的和功能
  • 松耦合: 服务将依赖关系最小化
  • 服务抽象:服务只发布七月,面向客户端隐藏内部逻辑
  • 服务重用性: 一种服务可以被其他服务所重用
  • 服务自治性: 服务自行控制环境与资源以保持独立性,这有助于保持服务的一致性和可靠性
  • 服务无状态性:服务负责消费方的状态管理,这不能与服务的自治性发生冲突
  • 服务可发现性: 客户可以通过服务元数据来查找和理解服务
  • 服务组合性:一种服务可以其他服务组合而成,而不管其他服务的大小和复杂性如何

命令和查询职责分离 CQRS Command-Query Responsibility Segregation

一个方法要么是执行某种动作的命令,要么是返回数据的查询,而不能两者皆是。换句话说,问题不应该对答案进行修改。更正式的解释是:一个方法只有在具有参考透明性时才能返回数据,此时该方法不会产生副作用

如何处理最终一致性的查询模型,最坏的情况考虑。

事件驱动架构

是一种用于处理事件生成、发现和处理等任务的软件架构。 如果事件、信息能够被过滤或者路由,消息订阅方将会得到充分的释放。 消息的管道和过滤器模式,

长处理过程- 事件驱动、分布式的并行处理模式 Saga

设计长时处理过程的三种不同方法:

  • 将处理过程设计成一个组合任务,使用一个执行组件对任务进行跟踪,并对各个步骤和任务完成情况进行持久化,我们将详尽地讨论这种方法
  • 将处理过程设计成一组聚合,这类聚合在一系列的活动中相互协作,一个或多个聚合实例充当执行组件并维护整个处理过程
  • 设计一个无状态的处理过程,其中每一个消息处理组件都将对所接受到的消息进行扩充,即向其中加入额外的数据信息,然后再将消息发送到下一个处理组件

对于跟踪有些长时间处理过程来说,我们需要考虑时间敏感性,在过程处理超时,我们既可以采用被动的,亦可以采取主动,回忆一下,状态跟踪器可以包含处理过程时的时间戳。如果再向追踪器增加一个最大允许处理事件,那么执行器便可以管理那些对事件敏感的长时处理过程

事件源

数据复制

持续查询

实体

一个实体是一个唯一的东西,并且可以在相当长的时间内持续的变化。我们可以对实体做多次修改,但由于它们拥有相同的身份标识,它们依然是同一个实体

值类型

领域服务

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

一个基本原则是就是我们应该尽量避免在聚合中使用资源库

请不要过于倾向于讲一个领域概念建模成领域服务,而是只有在有必要的时候才这么做。过度地使用领域服务将导致贫血领域模型,即所有的业务逻辑都位于领域服务中,而不是实体和值对象中

领域事件

聚合的其中一个原则是:在单个事务中,只允许对一个聚合实例进行修改,由此产生的其他改变必须要在单独的事务中完成。

我们通常将领域事件用于维护事件的一致性。这样可以消除两阶段提交(全局事务),还可以支持聚合原则

模块

我们应该优先考虑使用模块而不是限界上下文,除非通用语言为我们展示出了明确的边界

聚合

Tell Don't Ask 或者 Law of Demeter

资源库

集成限界上下文

  • 网络是不可靠的
  • 总会存在时间延迟、有时甚至非常严重
  • 带宽是有限的
  • 不要假设网络是安全的
  • 网络拓扑结构将发生变化
  • 知识和政策在多个管理员之间传播
  • 网络传输是有成本的
  • 网络是异构的

领域驱动设计战术设计之间的关系

演进的领域驱动设计过程

image-20180826173319050

控制软件复杂度的原则

  • 分而治之、控制规模
    • Keep it Simple Stupid KISS原则
    • 单一职责原则
    • 在应对新需求时,不会直接去修改一个复杂的旧系统,而是通过添加新特性,然后对这些特性进行组合
  • 保持结构的清晰与一致
    • 整洁架构的目的在于识别整个架构不同视角以及不同抽象层次的关注点,并为这些关注点划分不同层次的边界,从而使得整个架构变得更为清晰。
  • 拥抱变化
    • 可进化性
    • 可扩展性,核心就是封装
      • 业务规则
      • 算法策略
      • 外部服务
      • 硬件支持
      • 命令请求
      • 协议标准
      • 数据格式
      • 业务流程
      • 系统配置
      • 界面表现
    • 可定制性

需求引起的软件复杂度

需求分为业务需求与质量属性需求,因而需求复杂度可以分为两个方面:技术复杂度与业务复杂度

技术复杂度来自需求的质量属性,诸如安全、高性能、高并发、高可用性等需求,为软件设计带来了极大的挑战,让人痛苦的是这些因素彼此之间可能又互相矛盾、互相影响。

业务复杂度对应了客户的业务需求,因而这种复杂度往往会随着需求规模的增大而增加,由于需求不可能做到完全独立,一旦规模扩大到一定程度,不仅产生了功能数量的增加,还会因为功能相互之间的依赖与影响使得复杂度产生叠加

技术复杂度与业务复杂度并非完全独立,二者混合在一起更让系统的复杂度变的不可预期,难以掌控。

领域驱动设计的应对措施

  • 隔离业务复杂度与技术复杂度

    • 确定业务逻辑与技术实现边界,从而隔离各自的复杂度
    • 理想状态下,应该保证业务规则与技术实现是正交的
    • DDD通过分层和六边形架构来确保业务逻辑与技术实现的隔离

  • 针对庞大而复杂的问题域,限界上下文采用了“分而治之”的思想对问题域进行了分解,有效地控制了问题域的规模,进而控制了整个系统的规模。