写点什么

深入浅出 DDD 编程

作者:百度Geek说
  • 2022-11-24
    上海
  • 本文字数:5604 字

    阅读完需:约 18 分钟


作者 | 刘嘿嘿、离夏、立羽


导读

最近几年,微服务拆分大行其道,在业务越来越复杂的情况下,许多业务纷纷抛弃了传统单体架构,拥抱微服务。但随着微服务的拆分结束,大家又发现了新的问题,比如服务间逻辑复杂,运维复杂性变高,微服务架构变得越来越难以管理,最终演化成大泥球架构。

而本文主要介绍如何通过 DDD 对微服务进行拆分,首先介绍了什么是 DDD,通过从分析 DDD 的优势,到如何通过 DDD 进行业务拆分,并且在最后通过代码样例的方式,深入浅出的为读者介绍了 DDD 代码的核心实现。帮助大家进一步的了解 DDD 应该如何落地。

全文 6271 字,预计阅读时间 16 分钟。

01 什么是 DDD

DDD(领域驱动设计),起源于 2004 年 Eric Evans 出版《领域驱动设计》,近些年由于微服务的兴起,大家逐渐对单体服务进行拆分。


但是随着微服务拆分,由于业务逻辑拆分不合理导致调用环路问题、重试风暴问题等等,都给系统造成了更多的风险,并且随着业务更加复杂微服务职责划分出现问题,则业务迭代效率变得越来越差,最终变成一个大泥球系统。


而 DDD 的优势便是指导业务进行微服务拆分,下面我们以会员中心为例来具体讲解一下如何进行业务拆分以及相关的代码实现。

02 使用 DDD 的优势是什么

2.1 语言统一,消除误解

很多时候未必产品经理才是最懂业务的那个人,例如某些 B 端服务很多时候是运营人员在向产品同学提需求,在经过产品经理的翻译后,才转化成一个需求文档,这样就会导致有时候产品经理并不能完全表达出实际的需求,这就会导致开发人员交付的软件无法达到预期。从而导致返工,浪费人力。


而 DDD 需要设计一种通用的语言,拉齐各个需求方的理解,一旦产品同学和技术同学对业务具备了相同的理解,统一的语言,那在后续的需求迭代种就会变得非常顺畅。


在改造初期我们耗费了非常大的精力向产品同学讲清楚哪些抽象应该定义为实体,实体与实体的关系是什么,在不断的沟通、磨合中,最好我们成功建立起了一些通用的语言,拉齐了产品经理、运营同学、开发人员的理解,最大幅度的消除了由于理解不一致导致的返工、重构等工作。

2.2 更专注于业务的战略设计

战略设计侧重于业务梳理,结合业务流程划分对应的核心域、通用域、支撑域。战略设计的核心价值是围绕产品规划重点投入资源,确保重点子业务可以确保得到足够的人力支持。

2.3 设计即代码,代码即设计

在过去的项目详细设计中,我们的重心在数据怎么存储?数据流通是什么样的。这样可能导致在设计文档和代码中就具备较大的 Gap,实现上就可能有问题。


而 DDD 倡导的是思考,而不是写代码。在代码设计之前定义好领域语言,和领域专家沟通无碍,定义好领域规则,这样在写代码的时候留下较少的思考。代码只是把设计文档翻译成代码,写代码更像是在照着设计文档在做填空题,只需要将代码填到指定的文件中即可。

03 如何使用 DDD

3.1 DDD 战略设计

3.1.1 划分核心域,通用域、支撑域

在实际的工作中,很多产品经理会陷入到各种繁杂的业务指标中,无法从繁杂的业务中抽身,定义好哪些是重要的模块,或者无法表达出业务各个模块中最重要的是什么。这种情况就会导致每个,产品没有这就会导致在人员分工、资源申请上出现一些问题。


做战略设计,最核心的事情就是划分清楚核心域,通用域、支撑域,我们把更多的精力投入到核心的问题中,而不被大量次要的问题淹没。


  • 核心域:业务最核心的部分,这部分需要产品同学确定,例如,从长线来看我们主要核心做的投入,是做流量引入,还是做变现

  • 支撑域:业务中非核心的部分,若产品确定现有核心域是流量引入,那在流量变现部分业务,就是支撑域

  • 通用域:例如登录验证、验证码、支付能力等则更多的使用公司内部的中台能力,若公司没有通用的中台能力,我们也会以建设中台的思路自建一个内部的中台服务


本处仅仅描述我们对于战略设计理解,不对战略设计展开说明。

3.1.2 划分边界

微服务职责的划分是执行环节的第一步,也是最重要的一步,尤其从大单体拆分为多个微服务时,需要考虑以下几点:


  1. 要通过领域驱动划分边界,若暂时考虑不清楚边界,那就先不要拆分;

  2. 明确微服务分层,上游服务只能对下游服务产生依赖,防止微服务环路调用问题,同时下游服务需要考虑重试风暴问题;

  3. 核心域的微服务需要具备故障降级,容灾能力;

  4. 要基于组织架构进行边界的划分,微服务的梳理其实也是团队的梳理,过度的拆分可能导致更多的沟通成本。

3.2 DDD 战术设计

3.2.1 名词解释

聚合与聚合根:是一组相关对象的组合,可以作为拆分微服务的最小单位,具有高内聚、低耦合的特点,聚合在 DDD 中是一个很重要的概念,核心领域往往都需要用聚合来表达;聚合根为其根节点,聚合根有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。


领域服务:一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。


领域事件:领域事件是对领域内发生的活动进行的建模。


实体:多个属性、行为及操作的载体,实体有全局唯一性标识(ID),有独立的生命周期。例如会员用户中,每个会员都可以被认为一个实体,都有 userid 唯一性标识。


值对象:通过对象属性来识别的对象,没有标识符概念,无生命周期,只描述业务属性。如在一个会员系统中,会员权益信息集合即可看为一个值对象,只用于对权益属性的描述,只有数据初始化操作和有限的不涉及修改数据的行为。

3.2.2 如何进行战术设计

接下来我们以会员中心为例为大家详细介绍。


在战略模型中我们已经划分清楚边界,梳理不同领域及相关关系。接下来我们需要从战术层面上剖析领域模型内部之间的关系,对会员上下文进行建模(下文为简化版)。



在会员上下文中,我们以会员实体为中心,通过会员(vipinfo)这个聚合根来控制会员权限,一个会员包括用户 ID(uid)、会员权益(Privilege)、所属机构(tp)以及会员码(vip_code),而会员码针对订单维度分别对应不同的权益内容(privilege)。


这些值对象不具有业务行为特征,只关心本身属性值。会员实体具有业务行为及业务逻辑,例如会员入驻、会员变更、会员绑码等,外部访问会员权益值对象等都需要通过会员实体来进行。


在会员域中,我们同时支持会员码及会员维度的领域服务,包括购买、获取会员信息、绑码等服务。

3.3 DDD 代码实现

3.3.1 项目介绍

  • 会员微服务主要实现获得会员信息、会员码信息、绑会员码、会员码退款等操作。

  • 服务使用 ddd 四层架构,分为接口层、应用层、领域层和基础层。

  • 本服务因为业务复杂性较低,为减少冗余代码,使用松散分层。(架构根据耦合的紧密程度又可以分为两种:严格分层架构和松散分层架构。严格分层:任何层只能依赖与他相邻的下层。松散分层:任何层可以依赖任意他的下层。)

3.3.2 项目结构

项目结构如图所示分为四层、对应到到代码目录上(附录 1),代码一级目录有 interface(接口层)、application(应用层)、domain(领域层)、infrastructure(基础层)四个目录。


接口层:

接口层处理接口定义、批处理相关逻辑。目录如下:


|-- interface|   |-- command // 批处理接口层|   |   |-- controller |   |   |   `-- vip|   |   |       |-- add.go |   |   |       `-- update.go|   |   |-- router.go // 代码入口定义|   |   `-- script.go|   `-- http // api接口层|       |-- controller // 接口入参校验、定义,调用下层代码|       |   |-- lawyer|       |   |   |-- add.go|       |   |   `-- update.go|       |   `-- vipcode|       |       |-- add.go|       |       `-- update.go|       |-- router.go // api路由
复制代码



interface 目录下有 command、http 两个目录,其中,


command:包含批处理入口,批处理路由,编排批处理相关领域层服务、事件、实体和基础层相关函数。批处理代码无需应用层直接依赖领域层、基础层,降低代码冗余度。


http:包含接口路由、定义,接口入参校验、定义。

应用层:

主要负责组织、编排领域层服务、事件、实体和基础层相关函数。


application 下有 service、viewmodel。


|-- application|   |-- service //应用层服务|   |   |-- lawyer|   |   |   |-- add.go|   |   |   `-- update.go|   |   `-- vip|   |       |-- add.go|   |       `-- update.go|   `-- viewmodel // 视图|       |-- lawyer|       |   |-- transform.go // 转化函数|       |   `-- vm.go //视图数据结构|       `-- vip|           |-- transform.go|           `-- vm.go
复制代码


service:对多个领域服务、基础层 ral 调用、数据持久化服务进行封装、编排,为上层提供更粗粒度的服务,调用领域层服务,仓储和事件,因为松散分层结构,也可以调用基础层服务。


viewmodel:为上层多变的数据结构要求,提供相应视图定义和实体到视图的转化方法。

领域层:

领域层存放业务核心逻辑包括聚合根、实体、值对象、仓储接口、领域服务、领域事件接口等。


领域层下分有五个目录:


|-- domain|   |-- aggregate // 聚合|   |   |-- lawyer|   |   |   |-- entity.go // 实体定义|   |   |   `-- vo.go // 值对象|   |   `-- vipcode|   |       |-- entity.go|   |       |-- vo.go|   |-- event // 领域事件|   |   `-- vipcode|   |       `-- order.go|   |-- repository // 仓储接口|   |   |-- lawyer.go|   |   `-- vipcode.go|   |-- adaptor // 防腐层|   |   `-- sms.go|   `-- service // 领域服务|       |-- lawyer|       |   `-- vipcode.go|       `-- vipcode|           `-- vipcode.go
复制代码


aggregate:放置聚合根,实体、值对象数据结构定义,以及相关初始化代码。


领域内数据流转处理依赖,相关聚合根,下游服务发生改变——如数据表结构变换,只需将相关数据转化为业务定义聚合根,代码更改只需在基础层,不涉及上层。


下面是会员码实体示例,里面又包含有订单值对象,会员码机构值对象和会员码权益值对象。


// EntityVipCode 会员码实体(简化版本)type EntityVipCode struct {  ValidityStart *time.Time       // 绑码开始时间  ValidityEnd   *time.Time       // 绑定会员码结束时间  OrderInfo     *VOOrderInfo     // 订单信息值对象  BuyerInfo     *VOCodeBuyerInfo // 买会员码机构信息  PrivilegeInfo *VOPrivilege     // 会员码包含的权益}
复制代码


event:放置基础层事件抽象的接口——为了实现依赖倒置。


repository:放置基础层数据持久化服务抽象的接口。


service:存放一下领域服务代码,向应用层服务提供方法调用,依赖倒置在 ddd 中使用频繁 。


adaptor:存放防腐层数据结构定义、转化函数。


防腐层在下游服务和上游服务之间,将下游服务翻译为上游服务语言,抛去无需关注的,防止上层服务掺杂过多无需关注杂质。


ddd 中广泛应用了依赖倒置原则(即调用要依赖于抽象接口,不要依赖于具体实现),减少 ddd 各层之间的耦合性,提高系统的稳定性,减少并行开发风险,提高代码的可读性和可维护性,非常适合 ddd 这样为应对频繁迭代的设计思想。


如下创建订单体现依赖倒置思想,无需关注具体实现,假若使用订单方发生了变更(如更换服务提供方、服务提供方更换实现逻辑或者服务实现逻辑更改了),我们在上层代码只需要更改传入的参数,无需关注其他变更。


// ReqCreateOrder 创建订单func ReqCreateOrder(ctx context.Context, vipRepo repository.IVipCodeRepo, vipcodeentity vipcode.EntityVipCode) (*order.PreorderRetData, error)
type IVipCodeRepo interface { CreateOrder(ctx context.Context, ev vipcode.EntityVipCode) (*liborder.PreorderRetData, error) UpdateVipCode(ctx context.Context, patch map[string]interface{}, conditions map[string]interface{}) (int64, error)}
复制代码
基础设施层:

基础层存放领域事件、数据持久化、ral 调用相关代码。


其下有三个目录:


|-- infrastructure|   |-- event // 领域事件实现|   |   |-- init.go|   |   `-- vipcode|   |       `-- consume_order.go|   |-- persistence // 持久化存储实现|   |   |-- init.go|   |   |-- lawyer|   |   |   |-- po.go|   |   |   |-- repo.go|   |   |   `-- transform.go|   |   `-- vipcode|   |       |-- po.go|   |       |-- repo.go|   |       `-- transform.go|   `-- rpc // rpc调用实现|       |-- db|       |-- init.go|       |-- redis
复制代码


event:领域事件具体实现,依赖 rpc 服务。


persistence:放置储存持久化代码,数据库存储对应 PO,和 PO 到实体的转化方法,所有具体实现方法都出参都需要转化成实体供给上层使用。


rpc:rpc 远程调用其他微服务、消息中间件等服务代码。

04 总结

用好 DDD 的关键,就是理解 DDD 和核心思想,其本质也是面向对象的设计方法,即是把业务模型转换为对象模型从而来控制业务持续变化而导致系统的复杂性,使得系统更加具有可扩展性、可维护性。


在相对比较小、逻辑简单的微服务,在代码实现层面,我们并没有按照 DDD 进行开发,传统的 MVC 足以应对,若强行使用 DDD 则会徒增大家的工作量。


DDD 的核心是通过战略设计来匹配产品层面的业务规划,在战术设计层面通过对每个模块进行抽象、建模来完成业务梳理划分边界,在代码实现层面来完成设计文档到代码的映射,做到设计即代码、代码即设计。


而 DDD 只适用于大型的、复杂的业务场景。切勿为了 DDD 而 DDD。


————END————


参考资料


[1] 《领域驱动设计》


[2]《实现领域驱动设计》


[3]https://mp.weixin.qq.com/s/y57l-PhzibAjjL3EzPqSow


[4]https://mp.weixin.qq.com/s/_ggIPOvB-ptBanbqqKULxQ


[5]https://mp.weixin.qq.com/s/jU0awhez7QzN_nKrm4BNwg


推荐阅读:


百度APP iOS端内存优化实践-内存管控方案


Ernie-SimCSE对比学习在内容反作弊上应用


质量评估模型助力风险决策水平提升


合约广告平台架构演进实践


AI技术在基于风险测试模式转型中的应用


Go语言躲坑经验总结

发布于: 刚刚阅读数: 5
用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
深入浅出DDD编程_架构_百度Geek说_InfoQ写作社区