1.1. DDD 领域驱动设计

1.1.1. DDD是什么

什么是领域

在很长一段时间里,我们认为技术是主导项目成功的关键因素,这种关键因素通常表现在项目使用的编程语言、框架、架构、中间件、数据库等等方面。但技术真的是项目成功的关键因素吗? 在一个软件项目里除了技术层面的这部分,我们最主要的事情是实现业务。实现业务其实是在实现所在业务领域中所需要的业务。技术也是一个领域,称之为技术领域。领域驱动设计中的领域是指的业务领域

什么是子域

子域跟领域是包含关系,多个 业务域(子域)的组合形成了 一个更大的业务域(领域)。

image

1.1.2. DDD解决了什么问题

(一)宏观角度 - 过度耦合引发的系统腐败。

随着迭代的不断演化,业务逻辑变得越来越复杂,我们的系统也越来越冗杂。模块彼此关联,有的时候都很难说清模块的具体功能意图是啥。修改一个功能时,往往光回溯该功能需要的修改点就需要很长时间,更别提修改带来的不可预知的影响面。

image

例如上述交易服务几乎集合了所有场景的交易接口。同时我们的表也是一个大表,几乎包含了所有的交易场景的数据。在我们维护代码时,牵一发而动全身,很可能只是想改下罚金相关的功能,却影响到了车费支付等核心路径。

上述问题,归根到底在于系统架构不清晰,划分出来的模块内聚度低、高耦合。

领域模型解决方案 - 战略建模,划分界限上下文。

(二)微观角度 - 贫血症引发的失忆症。

贫血领域对象 - 对象只用作数据的载体,没有行为和动作。

举一个抽奖活动的例子:奖池里配置了很多奖项,根据概率匹配对应奖项即可。 奖池和奖项的库表配置如下:

image

根据需求,按照以往思路会设计AwardPool(奖池)和Award(奖项)两个对象,然后将业务逻辑统一封装在service层中。

image

image

按照通常思路实现,可以发现:业务逻辑都是写在Service中的,Award和AwardPool充其量只是个数据载体,没有任何行为。 简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症。

领域模型解决方案 - 战术建模,细化界限上下文。将数据和行为封装在一起,并与现实世界中的业务对象相映射。

上述例子,根据概率选择奖品的逻辑应当放到AwardPool类中。

1.1.3. DDD建模工具

(一)宏观角度 - 战略建模,划分界限上下文

>> 什么是限界上下文

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

一个形象的隐喻: 细胞质所以能够存在,是因为细胞膜限定了什么 在细胞内,什么在细胞外,并且确定了什么物质 可以通过细胞膜。

image

>> 划分界限上下文

限界上下文应该从需求出发,按领域划分。

我们实践是:从需求出发,从产品所讲的通用语言中提炼出概念对象,寻找对象之间的联系,将紧耦合的各自圈在一起,观察他们内在的联系,从而形成对应的界限上下文。

形成之后,我们可以尝试用语言来描述下界限上下文的职责,看它是否清晰、准确、简洁和完整。

>> 具体案例 - 抽奖活动的领域模型思路

产品需求概述:

  1. 抽奖活动有活动限制,例如用户的抽奖次数限制,抽奖的开始和结束的时间等;
  2. 一个抽奖活动包含多个奖品,可以针对一个或多个用户群体;
  3. 奖品有自身的奖品配置,例如库存量,被抽中的概率等,最多被一个用户抽中的次数等等;
  4. 用户群体有多种区别方式,如按照用户所在城市区分, 按照新老客区分等;
  5. 活动具有风控配置,能够限制用户参与抽奖的频率。

根据产品的需求,我们提取了一些关键性的概念 作为子域,形成我们的限界上下文,如下:

抽奖上下文 - 核心抽奖逻辑、奖品发放等。 活动准入上下文 - 开始/计数时间、可参与次数、 客群管理、用户/城市画像等。 库存上下文 - 奖品配置、库存核销等。 风控上下文 - 防刷单、安全等。 计数上下文 - 奖品发放次数/金额计数、风控计数等。 活动展示上下文 - 支付CMS管理、活动管理。

image

(二)微观角度 - 战术建模,细化界限上下文

梳理清楚上下文之间的关系后,我们需要从战术层面上剖析上下文内部的组织关系。首先看下DDD中的一些定义。

领域对象-实体:当一个对象由其标识(而不是属性)唯一区分时,这种对象称为实体(Entity),实体可以有行为。 领域对象-值对象:当对一个对象的描述没有唯一标识时,它被称作值对象(VO),值对象不能有行为。值对象可以仅仅理解为实际的Entity对象的一个属性结合而已。 领域对象-聚合根:是一组相关对象的集合,作为一个整体被外界访问。聚合由根实体,值对象和实体组成。 领域服务:当我们采用了微服务架构风格,一切领域逻辑的对外暴露均需要通过领域服务来进行。 领域事件:领域事件是对领域内发生的活动进行的建模。 防腐层:一个上下文通过一些适配和转换与另一个上下文交互。

image

image

在抽奖上下文中,我们通过抽奖(DrawLottery)这个聚合根来控制抽奖行为,可以看到,一个抽奖包括了抽奖ID(LotteryId)以及多个奖池(AwardPool),而一个奖池针对一个特定的用户群体(UserGroup)设置了多个奖品(Award)。 另外,在抽奖领域中,我们还会使用抽奖结果(SendResult)作为输出信息,使用用户领奖记录(UserLotteryLog)作为领奖凭据和存根。

1.1.4. DDD应用实践

我们开始在工程中真正落地DDD。

>> 模块

在我们的工程中,一般用一个模块来表示一个领域的限界上下文。 一般的工程中包的组织方式为 {com.公司名.组织架构.业务.上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部。

image

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

image

>> 领域对象

解决对象的贫血问题,将对象的行为内聚到领域对象中。

image

与以往的仅有getter、setter的业务对象不同,领域对象具有了行为,对象更加丰满。同时,比起将这些逻辑写在服务内(例如**Service),领域功能的内聚性更强,职责更加明确。

>> 资源库

领域对象需要资源存储,存储的手段可以是多样化的,常见的无非是数据库,分布式缓存,本地缓存等。资源库的作用是对领域的存储和访问进行统一管理。

image

资源库对外的整体访问由资源库提供,它聚合了各个资源库的数据信息,同时也承担了资源存储的逻辑(例如缓存更新机制等)。

image

在抽奖资源库中,我们屏蔽了对底层奖池和奖品的直接访问,而是仅对抽奖的聚合根进行资源管理。

比起以往将资源管理放在服务中的做法,由资源库对资源进行管理,职责更加明确,代码的可读性和可维护性也更强。

>> 防腐层

亦称适配层。在一个上下文中,有时需要对外部上下文进行访问,通常会引入防腐层的概念来对外部上下文的访问进行一次转义。

image

在抽奖平台中,我们定义了用户城市信息防腐层(UserCityInfoFacade),用于外部的用户城市信息上下文。

>> 领域服务

上文中,我们将领域行为封装到领域对象中,将资源管理行为封装到资源库中,将外部上下文的交互行为封装到防腐层中。此时,我们再回过头来看领域服务时,能够发现领域服务本身所承载的职责也就更加清晰了,即就是通过串联领域对象、资源库和防腐层等一系列领域内的对象的行为,对其他上下文提供交互的接口。

image

>> 领域内的数据流转

首先领域的开放服务通过信息传输对象(DTO) 来完成与外界的数据交互;

在领域内部,我们通过领域对象(DO) 作为领域内部的数据和行为载体;

在资源库内部,我们沿袭了原有的数据库 持久化对象(PO)进行数据库资源的交互;

同时,DTO与DO的转换发生在领域服务内, DO与PO的转换发生在资源库内。

image

1.1.5. 总结

一条业务线由研发、产品、业务共同协作和维护,大家只是在不同维度做同一件事情。

领域驱动设计的实现方式不仅仅是代码,也可以是PRD、业务模型图,选择一种大家都能看懂的方式,统一团队语言,从上层业务到底层实现都是同一个领域模型,减少信息传递经过翻译造成理解不一致。

本文是公司内部一同事分享,我从他笔记里摘抄出来的,写的挺清楚的

results matching ""

    No results matching ""