关于后端:深入浅出DDD编程

52次阅读

共计 6237 个字符,预计需要花费 16 分钟才能阅读完成。

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

导读

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

而本文次要介绍如何通过 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 语言躲坑经验总结

正文完
 0