Skip to content

DDD-Domain-Driven Design

有次面试被问到了

本文讨论的是及其复杂的场景下的解决方案,当然绝大多数公司压根就用不到DDD,也没有必要使用DDD。在业务初期,都是业务先行,普通的开发模式完全可以覆盖

DDD核心在 领域建模设计

什么情况下启用DDD?

过渡耦合

系统已经达到牵一发而动全身的程度了。原本只想改一个小功能,却不曾想影响核心接口的功能了。虽然重构可以解决,但是确实暂时的,只是解决了庞大系统中的一小部分痛点,长期来看,类似的问题并没有被彻底解决。

用DDD则可以解决领域模型到设计模型的同步、演化,最后再将反映了领域的设计模型转为实际的代码。

模型是我们解决实际问题所抽象出来的概念模型

  • 领域模型则表达与业务相关的事实
  • 设计模型则描述了所要构建的系统

什么是贫血症和失忆症

贫血领域对象(Anemic Domain Object)是指仅用作数据载体,而没有行为和动作的领域对象。—— 在大部分公司都是这样,对象只是数据的载体,没有行为。

业务逻辑复杂时,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。因为我做电商系统做的比较多,就经常会遇到这个问题,一个订单的状态散落的到处都是,如果对系统代码不熟悉或者对业务不熟悉,经常会奔波在修Bug的途中,不是这里缺少,就是那里缺少

解决大规模和复杂的软件可以通过哪些方法

  • 分而治之。将大的问题划分。模板就是在每一个分支内实现高内聚低耦合
  • 抽象。先不谈具体的实现,直接说出怎么可以做到。举一个简单的例子,我饿了,我要吃东西。而不是优先考虑我要吃什么,是吃面还是吃饭。这个就是一个抽象
  • 框架。DDD就是一种,按照成熟的框架,在这个框架内做好该做的事。所谓的框架更多的是一种限制,是一种高效。在大的框架内做事,绝对比自己或者靠团队的臆想靠谱的多,会减少很多不必要的低级错误。

分而治之

一般通过2个维度进行,即技术维度和业务维度。技术维度就是分层,例如我们常用的MVC模式,业务维度则考研对于业务的理解,需要划分出业务领域。

我们经常聊的微服务其实就是从业务维度来划分的。例如订单服务、商品服务等等。那这里的DDD和微服务的区别在哪里呢? 我们一般做架构设计时,主要集中在一下3个方面:

  • 业务架构: 根据业务需求设计业务模块及其关系
  • 系统架构: 设计系统和子系统的模块
  • 技术架构: 决定采用的技术及框架

熟悉了这3个方面之后,就可以说区别了。DDD的侧重点是业务架构,而微服务侧重于系统架构和技术架构,以一套系统架构和技术架构来承接所有的业务,在常规模式下会一直用很久;而DDD的业务架构,总是业务先行,在响应业务变化调整业务架构时,系统架构也会随着业务架构来调整,所以DDD是在一直服务着业务,一点点的逼迫技术靠近业务,而不是想微服务架构一样,吃老本。

不知道你们在日常工作中,会不会遇到这种场景,就是业务提一个需求,技术就是暂时的系统能力支撑不了。这个其实技术就很被动,也是微服务架构会带来的问题。如果使用DDD架构,则这种问题会少很多,也不是说完全没有哈,只是DDD这种模式的运行会让技术慢慢的贴合业务,所以当业务提出新的诉求时,才不会有那么多不能支撑的时候。

设计领域模型的一般步骤

  1. 根据需求划分出初步的领域和限界上下文,以及上下文之间的关系;
  2. 进一步分析每个上下文内部,识别出哪些是实体,哪些是值对象;
  3. 对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根;
  4. 为聚合根设计仓储,并思考实体或值对象的创建方式;
  5. 在工程中实践领域模型,并在实践中检验模型的合理性,倒推模型中不足的地方并重构。

限界上下文

一个由显示边界限定的特定职责。领域模型便存在于这个边界之内。在边界内,每一个模型概念,包括它的属性和操作,都具有特殊的含义。

如何划分限界上下文

实际操作过程中,要根据产品需求来,这是根本。当然这并不是一次就能搞定的事情,在后面的开发中可能还会不断的推翻及重构

清晰的上下文划分的好处

  1. 任务拆分方便
  2. 成员之间沟通方便
  3. 如果是跨团队的,则沟通便利

细化上下文

  • 实体: 当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。理解上来说就是id,例如,人可以通过身份证号这种独一无二的标识来进行区分。而不是人的属性,例如身高,年龄等
  • 值对象: 当一个对象用于对事物进行描述而没有唯一标识时,它被称作值对象(Value Object)。例如年龄28岁。使用值对象,可以更好的做系统优化和设计的精简优化。在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。
  • 聚合根: 聚合一组相关对象的集合,作为一个整体被外界访问,聚合根是这个聚合的根节点。聚合由根实体、值对象和实体构成。聚合是一个非常重要的概念,核心领域往往都需要用聚合来表达。其次,聚合在技术上有非常高的价值,可以指导详细设计。
  • 领域服务: 一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。当我们采用了微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。如原本由聚合根暴露的业务逻辑也需要依托于领域服务。
  • 领域事件: 领域事件是对领域内发生的活动进行的建模

如何创建好聚合?

  • 边界内的内容具有一致性:在一个事务中只修改一个聚合实例。如果你发现边界内很难接受强一致,不管是出于性能或产品需求的考虑,应该考虑剥离出独立的聚合,采用最终一致的方式。
  • 设计小聚合:大部分的聚合都可以只包含根实体,而无需包含其他实体。即使一定要包含,可以考虑将其创建为值对象。
  • 通过唯一标识来引用其他聚合或实体:当存在对象之间的关联时,建议引用其唯一标识而非引用其整体对象。如果是外部上下文中的实体,引用其唯一标识或将需要的属性构造值对象。 如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。

DDD的工程

落地

模块

一般的工程中包的组织方式为{com.公司名.业务.上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部。

import com.moatkon.bussiness.order.*; // 订单上下文
import com.moatkon.bussiness.goods.*; // 商品上下文

对于模块内的组织结构,一般情况下我们是按照领域对象、领域服务、领域资源库、防腐层等组织方式定义的。

import com.moatkon.bussiness.order.domain.valobj.*; // 值对象
import com.moatkon.bussiness.order.domain.entity.*; // 对象
import com.moatkon.bussiness.order.domain.aggregate.*; // 聚合根
import com.moatkon.bussiness.order.domain.service.*; // 领域服务
import com.moatkon.bussiness.order.domain.repo.*; // 资源库
import com.moatkon.bussiness.order.domain.facade.*; // 防腐层

上下文集成

通常集成上下文的手段有多种,常见的手段包括开放领域服务接口、开放HTTP服务以及消息发布-订阅机制等


  • 防腐层(适配层): 通过适配器和转换,和另一个上下文交互。做隔离
  • 在DDD的落地过程中,要灵活

参考: 领域驱动设计在互联网业务开发中的实践