写点什么

【得物技术】出价组 DDD 分层模型总结

用户头像
得物技术
关注
发布于: 2021 年 03 月 26 日
【得物技术】出价组DDD分层模型总结

在五彩石项目启动伊始,就决定采用领域驱动模型来设计业务框架,但是由于当时好多人对该模型的不熟悉,以及对一些设定的难以理解,导致初版的代码还是存在不少 mvc 的影子的。随着大家对该模型的逐渐了解,一致认为需要对业务代码做一些优化,于是组内在 8 月份启动了二次 DDD 改造的内部迭代,经过不断的灰度放量,近期已经全量放开了新版业务逻辑。

关于对 DDD 的理解,每个人都可以是不同的,并且依据不同的理解所写的业务框架也是不同的,只要在一定的范围内能够逻辑自洽,那是没问题的。在启动 DDD 改造前,组内也做过一些分享,制定了一些规范,本文就是对组内这些规范的一个总结。

interfaces (用户接口层)

流量请求入口。

用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户(H5/APP)、程序(API)、消息队列(MQ)、超时中心回调,自动化测试和批处理脚本等等。

在最初的设计中,只有用户的调用是放在这一层的,其他调用是放在了 application,这样导致了两个结果,分层不明确和入口分散。

所以本次将所有的流量入口全部放在了 interfaces 做收口。

application(应用服务层)

应用层连接用户接口层和领域服务层,它是很薄的一层,主要职能是协调领域层多个聚合完成服务的组合和编排。

应用服务层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。

应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务之间的服务组合和编排。

在设计和开发时,不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的微服务就会演化为传统的三层架构,业务逻辑会变得混乱。

举个精简的例子,假设出价分四步,

1、调用商品接口校验商品是否上架,

2、出价规则校验,

3、数据落地,

4、调用库存接口生成库存

那么应用层的主要功能就是编排这四步,不做业务处理。其中第 1 步和第 4 步放到基础层处理(基础层封装外部 RPC),第 2 步在规则域执行,第 3 步在出价域执行。

在初版的设计讨论中,有过一个定义是查询业务可以穿领域层,直达基础层,理由是纯查询的业务没有复杂的逻辑,仅仅是拿数据而已。但这也只是一个想法而已,经过实际的业务实践可知查询还是有很多的业务逻辑组装,而基础层是不处理业务逻辑的,拿到数据后只能在应用层做封装,导致了应用层过于厚重。同时也考虑到一个合理的规范性,约定同样的流程以后,会更好的推进 DDD。

domain(领域服务层)

领域服务层是由多个业务职责单一的聚合构成,实现核心的领域逻辑。

领域层的作用是实现领域核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。

领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。

什么是充血模型        

     与之对应的是贫血模型,我们经常使用的 MVC 模型中,实体类只定义属性,并没有定义实体行为。这种将数据与业务逻辑分离,其实是违反了 OOP 的封装特性,实际上是一种面向过程的编程风格。任意代码都可以修改实体的属性,那么实体的属性值就不受限制了。这其实是自下而上的设计思想,是 SQL 驱动(SQL-Driven)的开发模式。        

     而充血模型则是将该实体所有行为也进行了定义(比如 save,modify,remove 等操作)。任何想要修改实体属性的操作,必须要通过实体本身来实现。这是一种自上而下的设计思想,由具体的业务驱动开发,不需要关心底层的 SQL 实现。

实体和领域服务在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。

聚合根

聚合根是一种更大范围的封装,把一组在业务上不可分隔的实体和值对象聚合在一起,通过根实体的唯一标识对外提供能力。

实体

实体是领域中需要唯一标识的领域概念。相同的两个实体,如果唯一标识不一样, 那么即便实体的其他所有属性都一样,我们也认为它们两个是不同的实体,比如同一个用户对同一个 sku 同价格的两个库存实体,除了主键 ID 外,其余均相同,但这仍然是两个实体。

同时,不应该给实体定义太多的属性或行为,而应该寻找关联,将一些属性或行为转移到其他关联的实体或值对象上。 比如 Inventory 实体,会存储一些商品信息(sku、spu 等),由于商品信息是一个完整的有业务含义的概念,所以,我们可以定义一个 Commodity 对象,然后把 Inventory 实体中商品相关的信息转移到 Commodity 对象上。如果没有 Commodity 对象,而把这些商品信息直接放在 Inventory 对象上,并且如果对于一些其他的比如费用信息、仓库唯一码等信息也放到进去,会导致 Inventory 对象很混乱,结构不清晰,最终导致它难以维护和理解。

值对象

值对象就是上面所说的 Commodity 对象,并不是每一个值对象都必须有一个唯一标识,也就是说我们不关心对象是哪个,而只关心对象是什么。以 Commodity 对象为例,如果有两个 Commodity 的 spuId 是一样的,我们就会认为这两个 Commodity 是同一个。也就是说只要 spuId 一样,我们就认为是同一个商品。

infrastructure(基础设施层)

基础设施层是数据封装层,在这里获取各类数据,比如数据库,缓存,外部领域,外部接口等。

基础层是贯穿除领域层外所有层的,比较常见的功能还是提供数据库持久化和外部领域服务调用的。

基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。

比如说,在传统架构设计中,由于上层应用对数据库的强耦合,很多的架构演进中最担忧的可能就是换数据库了,因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的。那采用依赖倒置的设计以后,应用层就可以通过解耦来保持独立的核心业务逻辑。当数据库变更时,我们只需要更换数据库基础服务就可以了,这样就将资源变更对应用的影响降到了最低。

依赖倒置



Domain 层不再直接依赖 Infrastructure 层,而是引入了一个适配器模式(Port/Adapter),使用 DIP(Dependency Inversion Principle,依赖倒置)反转了 Domain 层和 Infrastructure 层的依赖关系,其关系如上图所示 Domain 层以接口的方式开放端口,让 Infrastructure 层去实现,这样设计的有点是 Domain 层的演变和进化完全是独立的,向上不受 Application 层影响,向下不受 Infrastructure 层影响。

举个例子,领域层是通过仓储接口(repository)获取基础资源的数据对象,仓储接口会调用仓储实现,具体的基础资源的数据处理过程是在仓储实现中完成的。这样做的好处是,避免将仓储实现的代码混入上层业务逻辑中。如果以后替换数据库,由于做了基础资源的个性的代码隔离,所以实现了应用逻辑与基础资源的解耦。在更换数据库时只需要更换仓储相关的代码就可以了,应用的逻辑不会受太大的影响。

分离领域(调用关系)



分层是为了各层独立演进的,上层使用下层定义的服务,而下层对上层一无所知,另外每一层对上层隐藏细节实现,依赖契约交互,独立层在技术方案调整时只要遵守契约则可以做到上层无感知迁移,这样也方便各层的维护和标准化工作。DDD 有两种架构,严格分层架构和松散分层架构。优化后的 DDD 分层架构模型就属于严格分层架构,任何层只能对位于其直接下方的层产生依赖。而传统的 DDD 分层架构则属于松散分层架构,它允许某层与其任意下方的层发生依赖,建议使用严格分层架构。

对象流转

interfaces 层: request、response

application 层:DTO(data transfer object)

domain 层:entity、VO(value object)

infrastructure 层:PO(persist object)

首先用户接口层通过 request/response 对象来进行跨进程间的交互数据;应用服务层使用(DTO)来进行数据交互;在领域内部,我们通过领域对象(entity/VO)作为领域内部的数据和行为载体;在基础设施层,我们使用持久化对象(PO)进行数据库资源的交互。

防腐层

也被称适配层或者转换层

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

有以下几种情况会考虑引入防腐层:

1.需要将上层的模型翻译成当前层可以理解的模型。比如上层定义了两个值 id 和 type,不同的 type 所对应的 id 含义不同,那么翻译的时候,可以直接根据 type 进行转换。


领域事件

领域事件是领域模型中非常重要的一环,领域事件将会导致进一步的业务操作,有助于实现业务的解耦,并完成业务闭环。

举例来说,领域事件是业务流程的一个中间步骤,比如出价领域出价成功后将通知库存域添加库存动作;也可能是批处理过程发生的事件,比如求购域批处理程序扫描求购业务表判断是否要的群体触发基础服务域的 push 服务告知求购用户最新的求购进展。

领域事件可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。通过领域事件+补偿机制来达到最终一致性,提高系统的稳定性和性能;

在一致性要求不高时,可以通过领域事件订阅器直接向消息队列发送事件。

对一致性要求高时,需要先将事件存储,然后通过后台线程加载并分发到消息队列。

以上就是出价组在二次 DDD 改造中的一些总结,下面以一个例子做结尾吧。

案例

销售库存占用



应用服务层主要是做流程编排等轻量逻辑,领域服务层完成领域内实际逻辑并持久化数据。


文:CJ BOYS

CJ 是出价的拼音首字母缩写,由于组内都是 Boy,于是团队起名 CJ BOYS。

关注得物技术,携手走向技术的云端


发布于: 2021 年 03 月 26 日阅读数: 13
用户头像

得物技术

关注

得物APP技术部 2019.11.13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
【得物技术】出价组DDD分层模型总结