定义
领域驱动设计(DDD)是一种基于模型驱动的软件设计方式。 它以领域为核心,分析领域中的问题,通过建立一个领域模型来有效的解决领域中的核心的复杂问题。
而在电商、物流、保险、金融等线上、线下结合的新型电子产业中,模型成熟,设计语言通用,适合落地实践DDD。
DDD 组成部分
DDD 包括「战略设计」和「战术设计」两部分:
- 战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
- 战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
总结来说,战略设计主要是「找到限界上下文」,也就是从大到小服务的边界。战术设计偏向实现,比如在找到限界上下文中的实体、值对象、实体等。
DDD 中的核心概念
领域
DDD 的「领域」就是业务边界内要解决的业务问题域。
领域被进一步划分,按照覆盖范围分的话,有领域和子域(对应更小的业务范围)。子域下面还有子域。
当人们在自然科学研究中遇到复杂问题时,通常的做法就是将问题一步一步地细分,再针对细分出来的问题域,逐个深入研究,探索和建立所有子域的知识体系。
举例:
- 确定研究对象,即研究领域,这里是一棵桃树。
- 第二步:对研究对象进行细分,将桃树细分为器官,器官又分为营养器官和生殖器官两种。
- 第三步:对器官进行细分,将器官细分为组织。比如,叶子器官可细分为保护组织、营养组织和输导组织等。这个过程就是 DDD 将子域进一步细分为多个子域的过程。
- 第四步:对组织进行细分,将组织细分为细胞,细胞成为我们研究的最小单元。细胞之间的细胞壁确定了单元的边界,也确定了研究的最小边界。
核心域/通用域/支撑域
子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
- 核心域:最重要,解决一个业务/项目的核心问题。比如在京东,核心域就是自营物流系统。
- 通用域:被多个模块共同使用。比如在京东,短信、日志、告警就是运营商提供,被用户、物流、下单等多个子域使用,属于通用域。
- 支撑域:个性化业务功能,且不被复用。
举例:
以业务线“平安社区”为例,以安防设备为集成基础。安防设备和物联网平台却都是市场成熟厂家提供,划分为通用域;平安社区业务场景如人车双验、异常人员识别等个性化应用属于业务线的支撑域;人员数据、行为数据富有长期价值,这些视为核心域。如果未来在某个细分场景做到了领先且拥有市场壁垒,这样的业务也可能会从支撑域调整为核心域。
所以,核心域、通用域、支撑域的划分本质是公司战略方向的体现,DDD是从战略到战术角度来进行架构设计的方法。
界定上下文
「通用语言」就是大家都认可的名词定义,它在一个范围内是有效的,这个范围就是「限界上下文」。同时,「领域」也在这个上下文中。
举个通用语言相关的例子,在电商中,对于商品来说。不同的阶段(上下文)中,有不同的名字。在销售阶段是商品、在物流阶段是货物。
举个界定上下文的例子:
子域还可根据需要进一步拆分为子子域,比如,支付子域可继续拆分为收款和付款子子域。拆到一定程度后,有些子子域的领域边界就可能变成限界上下文的边界了。
理论上限界上下文就是「微服务」的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
实体
在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。它是贯穿业务流程的业务数据模型,在每个业务节点它们的属性会变化(比如订单的状态会变化),但对象本身没有变化。
1、如何实现实体
在面向对象中,一个类就是一个实体,有属性、方法。通常采用「充血模型」。
2、如何存储实体
一个实体,可能对应着底层一张表/两张表。比如用户user,对应着底层的用户表、地址表。
DDD中强调,从业务关系出发考虑设计,而不是从表结构出发考虑设计;因此不一定遵循sql范式,反而no-sql可能更合适。
在DDD中,领域建模是比数据建模更加优先。
值对象
简单来说,值对象本质上就是一个集合。那这个集合里面有什么呢?若干个用于描述目的、具有整体概念和不可修改的属性。
那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。
下面举个例子,人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。如下所示:
这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。
聚合
实体一般对应业务对象,值对象主要是属性集合。它们都是个体。当业务和逻辑紧密关联的实体和值对象组合时,就形成了聚合。聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。
聚合根
1、定义
它不光是实体,还负责协调聚合内的实体和对象,按照业务规则协同完成业务逻辑。
同时,它还是「对外接口人」。外部其他聚合/实体,无法直接调用聚合中的实体,要通过聚合根来调用。
2、作用
重点体会聚合与领域的区别。
它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。
反应在数据对象的设计上,就是避免一个大对象,包含过多的属性。比如对于一个客户聚合:
- 聚合根就是客户实体,里面存着最重要的元信息,包括客户id、身份证、姓名
- 客户的其他信息,将创建对应的实体,比如地址、账户,并且这些实体通过id来和客户实体关联起来
- 在进行SQL查询时:
- 会通过客户id,将客户实体信息从sql中拿到,
- 通过关联查询拿到地址、账户
- 最终将其拼接成消费侧需要的数据返回,比如json
3、挑选方法
挑选聚合,找到一个业务上下文边界,里面涉及到的实体就能组成聚合。
聚合根不好找:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。
举例如下:
领域事件
定义
这种事件发生后通常会导致进一步的业务操作,在 DDD 中这种事件被称为领域事件。
比如注册用户就是一个领域事件,注册会,会触发邮件通知。
作用
解耦微服务和领域模型。
一次事务尽量只更新一个聚合状态,多个聚合状态通过领域事件更新。这种不要求实时一致性,保证最终一致性即可。
实现
分为微服务内的领域事件,以及跨微服务的领域事件。
对于微服务内的,领域事件发生后完成事件实体构建和事件数据持久化,发布方聚合将事件发布到「事件总线」,订阅方接收事件数据完成后续业务操作。
跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、消息中间件,甚至事件数据持久化时还可能需要考虑引入分布式事务机制等。
两者都可以采用直接调用的方式,前者是内部调用,后者是RPC调用。实现强一致性。
建立领域模型方法
「事件风暴」是建立领域模型的主要方法,它是一个从发散到收敛的过程。
它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。
从发散(开会讨论)到收敛(从大到小,依次找到限界上下文、聚合、聚合根、实体等)。
领域模型案例
这里就是要做一个用户中台。
产品愿景(要做什么)
产品愿景的主要目的是对产品顶层价值的设计,使产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。
在建模之前,项目团队要思考这样两点:
- 用户中台到底能够做什么?
- 它的业务范围、目标用户、核心价值和愿景,与其它同类产品的差异和优势在哪里?
业务场景(以终为始)
场景分析是从用户视角出发的,根据业务流程或用户旅程,采用用例和场景分析,探索领域中的典型场景,找出领域事件、实体和命令等领域对象,支撑领域建模。
事件风暴参与者要尽可能地遍历所有业务细节,充分发表意见,不要遗漏业务要点。
用户中台有这样三个典型的业务场景:
- 第一个是系统和岗位设置,设置系统中岗位的菜单权限;
- 第二个是用户权限配置,为用户建立账户和密码,设置用户岗位;
- 第三个是用户登录系统和权限校验,生成用户登录和操作日志。
我们可以按照业务流程,一步一步搜寻用户业务流程中的关键领域事件,比如岗位已创建,用户已创建等事件。场景分析时会产生很多的命令和领域事件。
我用蓝色来表示命令,用橙色表示领域事件,用黄色表示补充信息,比如用户信息数据来源于 HR 系统的说明。
领域建模(怎么做)
- 事件风暴。发散寻找业务方方面面。(画出上图)
- 找到每个限界上下文中的实体。找实体的过程中,通过值对象,来减少实体数量和表的数量。
如下图所示,绿色表示实体。
- 划分聚合,找聚合根。在这些实体中,将相关实体组成聚合,并且找到聚合中最重要的实体,作为聚合根。上面7个实体,可以归纳出6个聚合:
- 系统功能(包括系统、菜单)
- 岗位
- 用户信息
- 用户日志
- 账户
- 认证票据
- 划定限界上下文,根据上下文语义将聚合归类。并且产生领域和子域,子域本身形成了限界上下文,可以做成单独微服务。
- 用户信息域:管理用户基本信息、用户登录和操作日志
- 认证域:管理账户、票据
- 权限域:管理岗位、系统功能
- 按照高内聚、低耦合的思路,开始封装实现代码。